golang 单元测试中常用以下 4 个库方便测试代码的编写:
gostub 主要用来给变量、函数、过程打桩 但是给函数打桩时,需要做侵入式修改
convey 主要用途是用来组织测试用例的,提供了很多断言,兼容go test
,有 web ui ,保存代码可自动跑测试
gomock 主要用来给接口打桩的。 mockgen 可以生成对应的接口测试文件。
monkey 主要也是用来给变量、函数打桩的
gostub 打桩的原理式通过反射,所以要求调用 stub 函数传入第一个参数必须是指针,然而函数并没有指针的概念,所以需要对函数做侵入式修改。
monkey 打桩的原理则是在运行时通过汇编语句重写可执行文件,将待打桩函数或方法的实现跳转到桩实现,原理和热补丁类似。但是 moneky 不是线程安全的,不能用在并发测试中
首先什么是打桩,桩函数?
网络解释:
认识单元测试中的打桩
什么是桩
桩,或称桩代码,是指用来代替关联代码或者未实现代码的代码。如果函数 B 用 B1 来代替,那么, B 称为原函数, B1 称为桩函数。打桩就是编写或生成桩代码。
打桩的目的
打桩的目的主要有:隔离、补齐、控制。
隔离是指将测试任务从产品项目中分离出来,使之能够独立编译、链接,并独立运行。隔离的基本方法就是打桩,将测试任务之外的,并且与测试任务相关的代码,用桩来代替,从而实现分离测试任务。例如函数 A 调用了函数 B ,函数 B 又调用了函数 C 和 D ,如果函数 B 用桩来代替,函数 A 就可以完全割断与函数 C 和 D 的关系。
补齐是指用桩来代替未实现的代码,例如,函数 A 调用了函数 B ,而函数 B 由其他程序员编写,且未实现,那么,可以用桩来代替函数 B ,使函数 A 能够运行并测试。补齐在并行开发中很常用。
控制是指在测试时,人为设定相关代码的行为,使之符合测试需求。
个人理解:打桩有点像 mock 并替换一个在测试中用到的对象的值或行为,桩函数即为 mock 函数,与被 mock 的函数签名一样;桩变量即为被 mock 替换值后的变量;打桩就是替换了原来对象的值或行为,但你仍然使用的是原来的对象在运行代码,原始的对象被暂时屏蔽并替换为了打桩后的 mock 对象。
在看网上相关示例代码时,有的直接使用 import . "xxx"
来省略写包名,个人不推荐这样用。
其次,import _ "xxx"
和 import . "xxx"
是不同的含义:
_
代表不引入包但会执行包的 init 函数.
代表写代码时可以省略包前缀,类似省略的别名,可以用别名替换包名
gostub 打桩全局变量示例
package main
import (
"fmt"
"github.com/prashantv/gostub"
)
var counter = 100
func stubGlobalVariable() {
stubs := gostub.Stub(&counter, 200)
defer stubs.Reset()
fmt.Println("Counter:", counter)
// 可以多次打桩
stubs.Stub(&counter, 10000)
fmt.Println("Counter:", counter)
}
func main() {
stubGlobalVariable()
}
// output:
// Counter: 200
// Counter: 10000
gostub 的 Stub 方法不能直接打桩一个函数,也就是第一个参数不能直接传函数名加取地址符号,第一个参数必须是一个变量的指针( must be a pointer to the variable )。
直接传函数名加取地址符号 Golang 是无法获取函数地址的( Golang cannot take the address of xxFunction )
gostub 打桩函数示例:
package main
import (
"fmt"
"github.com/prashantv/gostub"
)
// Greet return "hello,xxx"
func Greet(name string) string {
return "hello," + name
}
func main() {
var GreetFunc = Greet
fmt.Println("Before stub:", GreetFunc("axiaoxin"))
stubs := gostub.Stub(&GreetFunc, func(name string) string {
return "fuck u," + name
})
defer stubs.Reset()
fmt.Println("After stub:", GreetFunc("axiaoxin"))
// Output:
// Before stub: hello,axiaoxin
// After stub: fuck u,axiaoxin
}
对于打桩没有参数,返回值为一个或多个简单固定值的函数可以使用 StubFunc 方法:
package main
import (
"fmt"
"github.com/prashantv/gostub"
)
// Greet return "hello,xxx"
func Greet(name string) string {
return "hello," + name
}
func main() {
var GreetFunc = Greet
fmt.Println("Before stub:", GreetFunc("axiaoxin"))
// StubFunc 第一个参数必须是一个函数变量的指针,该指针指向的必须是一个函数变量,第二个参数为函数 mock 的返回值
stubs := gostub.StubFunc(&GreetFunc, "fuck u,axiaoxin")
defer stubs.Reset()
fmt.Println("After stub:", GreetFunc("axiaoxin"))
// Output:
// Before stub: hello,axiaoxin
// After stub: fuck u,axiaoxin
}
StubFunc 内部其实就是 Stub 函数的一个封装,将本来要自己写的函数参数封装为只需要传返回值。
以上将函数赋值给一个变量使用函数变量进行打桩的用法同样适用于标准库函数,例如我要替换标准库 time.Now
获取的当前时间值为当前时间 +1 小时:
package main
import (
"fmt"
"time"
"github.com/prashantv/gostub"
)
func main() {
var XNow = time.Now
fmt.Println("Before stub:", XNow())
stubs := gostub.Stub(&XNow, func() time.Time {
d, _ := time.ParseDuration("+1h")
return time.Now().Add(d)
})
defer stubs.Reset()
fmt.Println("After stub:", XNow())
// Output:
// Before stub: 2020-06-02 18:03:25.791691 +0800 CST m=+0.000065259
// After stub: 2020-06-02 19:03:25.791843 +0800 CST m=+3600.000217139
}