编写可测试的代码可能比编写单元测试本身更加重要,可测试的代码简单来说就是指可以很容易的为其编写单元测试代码。
参数最小依赖原则
剔除干扰因素
func judgeRate() int {
now := time.Now()
switch hour := now.Hour(); {
case hour >= 8 && hour < 20:
return 10
case hour >= 20 && hour <= 23:
return 1
}
return -1
}
这个函数内部使用了time.Now()来获取系统的当前时间作为判断的依据,看起来很合理。
但是这个函数现在隐式包含了一个不确定因素——时间。在不同的时刻我们调用这个函数都可能会得到不一样的结果。想象一下,我们该如何为这个函数编写单元测试呢?
如果不修改系统时间,那么我们就无法为这个函数编写单元测试,这个函数成了“不可测试的代码”(当然可以使用打桩工具对time.Now进行打桩,但那不是本文要强调的重点)。
接下来我们该如何改造它?
我们通过为函数传参数的方式传入需要判断的时刻,具体实现如下。
// judgeRateByTime 报警速率决策函数
func judgeRateByTime(now time.Time) int {
switch hour := now.Hour(); {
case hour >= 8 && hour < 20:
return 10
case hour >= 20 && hour <= 23:
return 1
}
return -1
}
其他类似的还需要注意有:时间、随机数、并发性、基础设施、现存数据、持久化、网络。
接口抽象进行解耦
依赖注入代替隐式依赖
在应用程序中使用全局变量的方式引入日志库或数据库连接实例。
var log = logrus.New()
type App struct{}
func (a *App) Start() {
log.Info("app start ...")
}
func (a *app) Start() {
a.Logger.Info("app start ...")
// ...
}
func main() {
app := &App{}
app.Start()
}
上面的代码中 App 中通过引用全局变量的方式将依赖项硬编码到代码中,这种情况下我们在编写单元测试时如何 mock log 变量呢?
此外这样的代码还存在一个更严重的问题——它与具体的日志库程序强耦合。当我们后续因为某些原因需要更换另一个日志库时,我们该如何修改代码呢?
我们应该将依赖项解耦出来,并且将依赖注入到我们的 App 实例中,而不是在其内部隐式调用全局变量。
type App struct {
Logger
}
func (a *App) Start() {
a.Logger.Info("app start ...")
// ...
}
// NewApp 构造函数,将依赖项注入
func NewApp(lg Logger) *App {
return &App{
Logger: lg, // 使用传入的依赖项完成初始化
}
}
依赖注入就是指在创建组件(Go 中的 struct)的时候接收它的依赖项,而不是它的初始化代码中引用外部或自行创建依赖项。
网友评论