测试框架整理
Golang主要有以下四个测试框架:
- GoConvey
- GoStub
- GoMock
- Monkey
1、GoConvey简介
测试案例
用于测试的函数,判断两个字符串切片是否相同:
func StringSliceEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
// []string{}和[]string(nil),这时两个字符串切片的长度都是0,但不相等
if (a == nil) != (b == nil) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
测试代码 :
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestStringSliceEqual(t *testing.T) {
Convey("TestStringSliceEqual should return true when a != nil && b != nil", t, func() {
a := []string{"hello", "goconvey"}
b := []string{"hello", "goconvey"}
So(StringSliceEqual(a, b), ShouldBeTrue)
})
}
执行结果:
=== RUN TestStringSliceEqual
TestStringSliceEqual should return true when a != nil && b != nil ✔
1 total assertion
--- PASS: TestStringSliceEqual (0.00s)
PASS
ok infra/alg 0.006s
一个函数多用例测试:
1、Convey非嵌套
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestStringSliceEqual(t *testing.T) {
Convey("TestStringSliceEqual should return true when a != nil && b != nil", t, func() {
a := []string{"hello", "goconvey"}
b := []string{"hello", "goconvey"}
So(StringSliceEqual(a, b), ShouldBeTrue)
})
Convey("TestStringSliceEqual should return true when a == nil && b == nil", t, func() {
So(StringSliceEqual(nil, nil), ShouldBeTrue)
})
}
执行结果:
=== RUN TestStringSliceEqual
TestStringSliceEqual should return true when a != nil && b != nil ✔
1 total assertion
TestStringSliceEqual should return true when a == nil && b == nil ✔
2 total assertions
--- PASS: TestStringSliceEqual (0.00s)
PASS
ok infra/alg 0.006s
2、Convey嵌套
Convey语句可以无限嵌套,以体现测试用例之间的关系。需要注意的是,只有最外层的Convey需要传入*testing.T类型的变量t。
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestStringSliceEqual(t *testing.T) {
Convey("TestStringSliceEqual", t, func() {
Convey("should return true when a != nil && b != nil", func() {
a := []string{"hello", "goconvey"}
b := []string{"hello", "goconvey"}
So(StringSliceEqual(a, b), ShouldBeTrue)
})
Convey("should return true when a == nil && b == nil", func() {
So(StringSliceEqual(nil, nil), ShouldBeTrue)
})
})
}
执行结果:
=== RUN TestStringSliceEqual
TestStringSliceEqual
should return true when a != nil && b != nil ✔
should return true when a == nil && b == nil ✔
2 total assertions
--- PASS: TestStringSliceEqual (0.00s)
PASS
ok infra/alg 0.006s
Web界面
GoConvey不仅支持在命令行进行自动化编译测试,而且还支持在 Web 界面进行自动化编译测试。
$GOPATH/bin/goconvey
Web界面的主要功能:
- 可以设置界面主题
- 查看完整的测试结果
- 使用浏览器提醒等实用功能
- 自动检测代码变动并编译测试
- 半自动化书写测试用例
- 查看测试覆盖率
- 临时屏蔽某个包的编译测试
Skip
针对想忽略但又不想删掉或注释掉某些断言操作,GoConvey提供了Convey/So的Skip方法:
- SkipConvey函数表明相应的闭包函数将不被执行
- SkipSo函数表明相应的断言将不被执行
当存在SkipConvey或SkipSo时,测试日志中会显式打上"skipped"形式的标记:
- 当测试代码中存在SkipConvey时,相应闭包函数中不管是否为SkipSo,都将被忽略,测试日志中对应的符号仅为一个"⚠"
- 当测试代码Convey语句中存在SkipSo时,测试日志中每个So对应一个"✔"或"✘",每个SkipSo对应一个"⚠",按实际顺序排列
- 不管存在SkipConvey还是SkipSo时,测试日志中都有字符串"{n} total assertions (one or more
sections skipped)",其中{n}表示测试中实际已运行的断言语句数
摘自:https://www.jianshu.com/p/e3b2b1194830
2、GoStub简介
使用场景
- 基本场景:为一个全局变量打桩
- 基本场景:为一个函数打桩
- 基本场景:为一个过程打桩
- 复合场景:由任意相同或不同的基本场景组合而成
1. 为一个全局变量打桩
假设num为被测函数中使用的一个全局整型变量,当前测试用例中假定num的值为150,则打桩的代码如下:
stubs := Stub(&num, 150)
defer stubs.Reset()
stubs是GoStub框架的函数接口Stub返回的对象,该对象有Reset操作,即将全局变量的值恢复为原值。
2. 为一个函数打桩
假设这是我们产品的既有代码中定义的函数:
func Exec(cmd string, args ...string) (string, error) {
...
}
则Exec函数是不能通过GoStub框架打桩的。
若要想将Exec函数通过GoStub框架打桩,需要对该函数的声明做重构,即将Exec函数定义为匿名函数,同时将它赋值给Exec变量, 重构后的代码如下:
var Exec = func(cmd string, args ...string) (string, error) {
...
}
当Exec函数重构成Exec变量后,丝毫不影响既有代码中对Exec函数的调用。由于Exec变量是函数变量,所以我们一般将这类变
量也叫做函数。
现在我们可以对Exec函数打桩了,代码如下所示:
stubs := Stub(&Exec, func(cmd string, args ...string) (string, error) {
return "xxx-vethName100-yyy", nil
})
defer stubs.Reset()
3. 为一个过程打桩
当一个函数没有返回值时,该函数我们一般称为过程。很多时候,我们将资源清理类函数定义为过程。
我们对过程DestroyResource的打桩代码为:
stubs := StubFunc(&DestroyResource)
defer stubs.Reset()
测试案例
结合GoConvey,测试函数中嵌套两级Convey语句,第一级Convey语句对应测试函数,第二级Convey语句对应测试用例。在第二级的每个Convey函数中都会产生一个stubs对象,彼此独立,互不影响。
func TestFuncDemo(t *testing.T) {
Convey("TestFuncDemo", t, func() {
Convey("for succ", func() {
stubs := Stub(&num, 150)
defer stubs.Reset()
stubs.StubFunc(&Exec,"xxx-vethName100-yyy", nil)
var liLei = `{"name":"LiLei", "age":"21"}`
stubs.StubFunc(&adapter.Marshal, []byte(liLei), nil)
stubs.StubFunc(&DestroyResource)
//several So assert
})
Convey("for fail when num is too small", func() {
stubs := Stub(&num, 50)
defer stubs.Reset()
//several So assert
})
Convey("for fail when Exec error", func() {
stubs := Stub(&num, 150)
defer stubs.Reset()
stubs.StubFunc(&Exec, "", ErrAny)
//several So assert
})
Convey("for fail when Marshal error", func() {
stubs := Stub(&num, 150)
defer stubs.Reset()
stubs.StubFunc(&Exec,"xxx-vethName100-yyy", nil)
stubs.StubFunc(&adapter.Marshal, nil, ErrAny)
//several So assert
})
})
}
Gostub不适用的复杂情况
-
被测函数中多次调用了数据库读操作函数接口 ReadDb,并且数据库为key-value型。被测函数先是 ReadDb 了一个父目录的值,然后在 for 循环中读了若干个子目录的值。在多个测试用例中都有将ReadDb打桩为在多次调用中呈现不同行为的需求,即父目录的值不同于子目录的值,并且子目录的值也互不相等
-
被测函数中有一个循环,用于一个批量操作,当某一次操作失败,则返回失败,并进行错误处理。假设该操作为Apply,则在异常的测试用例中有将Apply打桩为在多次调用中呈现不同行为的需求,即Apply的前几次调用返回成功但最后一次调用却返回失败
-
被测函数中多次调用了同一底层操作函数,比如exec.Command,函数参数既有命令也有命令参数。被测函数先是创建了一个对象,然后查询对象的状态,在对象状态达不到期望时还要删除对象,其中查询对象是一个重要的操作,一般会进行多次重试。在多个测试用例中都有将 exec.Command 打桩为多次调用中呈现不同行为的需求,即创建对象、查询对象状态和删除对象对返回值的期望都不一样
摘自:https://www.jianshu.com/p/70a93a9ed186