基本的测试
// sayhello.go
func sayHello(name string) string {
return fmt.Sprintf("Hi %s", name)
}
// sayhello_test.go
// go test
// go test -v
func Test_sayHello(t *testing.T) {
expected := "Hi zhangsan"
if expected != sayHello("zhangsan") {
t.Error()
// t.Errorf("not expected: %s", expected)
// t.Fail()
// t.FailNow()
// t.Fatal()
// t.Fatalf("not expected: %s", expected)
}
}
这个程序定义了一个名为"sayHello"的函数,它接收一个字符串参数"name",并返回一个格式为"Hi name"的字符串。该程序还包含一个单元测试文件"sayhello_test.go",用于测试"sayHello"函数的功能。
在测试函数"Test_sayHello"中,我们首先定义了一个预期输出字符串"expected",然后调用"sayHello"函数并将返回结果与"expected"进行比较。如果比较结果不符合预期,测试函数将会调用t.Error()函数表示测试失败,也可以使用其他可选的测试失败方式,例如t.Errorf()、t.Fail()、t.FailNow()、t.Fatal()或t.Fatalf(),这些函数会在不同的条件下以不同的方式标记测试失败,以保证代码的质量和可靠性。
引入 testify 库
实际项目中为更好的组织、诊断测试以及 mock 对象的行为,通常会引入 testify 库:
go get -u github.com/stretchr/testify
重构一下 sayhello_test.go 如下:
// sayhello.go
func sayHello(name string) string {
return fmt.Sprintf("Hi %s", name)
}
// sayhello_test.go
// go test
// go test -v
func Test_sayHello(t *testing.T) {
expected := "Hi zhangsan"
assert.Equal(t, expected, sayHello("zhangsan"))
// assert.Equal(t, 7, len(sayHello("lisi")))
assert.Len(t, sayHello("lisi"), 7)
}
由于每次都写 assert.Func 都会把参数 t 穿进去,比较麻烦,因为可以用 t 构造一个 assert 对象出来:
// sayhello_test.go
// go test
// go test -v
func Test_sayHello(t *testing.T) {
checker := assert.New(t)
expected := "Hi zhangsan"
checker.Equal(expected, sayHello("zhangsan"))
// checker.Equal(7, len(sayHello("lisi")))
checker.Len(sayHello("lisi"), 7)
}
表格驱动和子测试
表格驱动测试不是一个新的工具或者语法,是一种编写干净清晰的测试的视角。
// 求解斐波那契数列
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}
// func Test_fib(t *testing.T) {
// checker := assert.New(t)
// checker.Equal(1, fib(1))
// checker.Equal(1, fib(2))
// checker.Equal(2, fib(3))
// checker.Equal(3, fib(4))
// checker.Equal(5, fib(5))
// }
func Test_fib_refactor(t *testing.T) {
checker := assert.New(t)
testcases := []struct {
expected int
input int
}{
{1, 1}, {1, 2}, {2, 3}, {3, 4}, {5, 5},
}
/*
=== RUN Test_fib_refactor
=== RUN Test_fib_refactor/expect_fib(1)_to_1
=== RUN Test_fib_refactor/expect_fib(2)_to_1
=== RUN Test_fib_refactor/expect_fib(3)_to_2
=== RUN Test_fib_refactor/expect_fib(4)_to_3
=== RUN Test_fib_refactor/expect_fib(5)_to_5
*/
for _, tc := range testcases {
checker.Equal(tc.expected, fib(tc.input))
}
/*
--- PASS: Test_fib_refactor (0.00s)
--- PASS: Test_fib_refactor/expect_fib(1)_to_1 (0.00s)
--- PASS: Test_fib_refactor/expect_fib(2)_to_1 (0.00s)
--- PASS: Test_fib_refactor/expect_fib(3)_to_2 (0.00s)
--- PASS: Test_fib_refactor/expect_fib(4)_to_3 (0.00s)
--- PASS: Test_fib_refactor/expect_fib(5)_to_5 (0.00s)
*/
for _, tc := range testcases {
t.Run(fmt.Sprintf("expect fib(%d) to %d", tc.input, tc.expected), func(t *testing.T) {
asserter := assert.New(t)
asserter.Equal(tc.expected, fib(tc.input))
})
}
}
在函数 Test_fib_refactor 中,测试用例被组织成了一个结构体数组 testcases,每个测试用例包含了期望的结果和输入参数。通过遍历 testcases 中的测试用例,程序会依次测试每个输入参数的结果是否和期望结果一致。程序还使用了 t.Run 函数来提高测试的可读性和可维护性,将每个测试用例的期望输出结果以及输入参数打印在测试结果中。
fuzz 模糊测试
// reverse.go
func reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
// reverse_test.go
// 使用种子数据测试
// go test -v
// 独立运行
// go test -run=Fuzz_reverse
// 生成随机测试数据
// go test -fuzz=Fuzz
func FuzzR_reverse(f *testing.F) {
testcases := []string{"Hello world", "我是中文"}
for _, tc := range testcases {
// 添加种子数据
f.Add(tc)
}
// 待测试的函数有多个参数依次再次申明
f.Fuzz(func(t *testing.T, input string) {
checker := assert.New(t)
rev := reverse(input)
dblRev := reverse(rev)
checker.Equal(dblRev, input)
})
}
模糊测试是单元测试和性能测试的补充,程序包括一个用于反转字符串的函数 reverse 和一个用于对其进行单元测试的测试文件 reverse_test.go。
reverse 函数通过将字符串转换为字节数组,从字符串的两端开始交换字符来实现反转操作。在单元测试文件 reverse_test.go 中,定义了一个名为 FuzzR_reverse 的函数,用于生成随机的输入字符串并对 reverse 函数进行模糊测试。
该函数使用 testing.F 类型的参数,并通过 f.Add 将一些种子数据添加到测试中。随后,使用 f.Fuzz 方法来模糊测试 reverse 函数。该方法将随机生成的字符串作为输入,使用 assert 包中的方法进行测试,并将结果与期望值进行比较。
在命令行中,可以使用不同的参数来运行测试。例如,使用 go test -v 可以显示每个测试用例的详细输出,而使用 go test -fuzz=Fuzz 可以生成随机测试数据并对其进行模糊测试。
跳过测试
func add(a, b int) int {
return a + b
}
// 很耗时的测试,加 short 参数跳过
// go test -short
func Test_add(t *testing.T) {
checker := assert.New(t)
// 调用 os.Getenv()、os.LookupEnv() 判断环境,执行 t.SkipNow/Skip/Skipf 也是常用的做法
/*
if ci, ok := os.LookupEnv("CI"); ok {
t.Skipf("add func skiped on %s env", ci)
}
*/
if testing.Short() {
// t.SkipNow()
t.Skip("add func skiped")
}
// spends many time, such as loop、network、write disk、sync cloud etc
time.Sleep(time.Hour)
/*
OUTPUT
=== RUN Test_spendManyTime
[T+0000ms]
--- PASS: Test_spendManyTime (3600.00s)
PASS
*/
checker.Equal(2, add(1, 1))
}
有时候需要跳过很耗时的测试,或者在某种特定环境中跳过某些测试,程序包含一个名为 Test_add 的测试函数,用于检查 add 函数是否能够正确计算两个整数的和。
测试函数使用了 Go 语言标准库的 testing 包进行测试。它首先使用 assert.New 函数创建一个断言对象来进行测试断言。然后,它检查 -short 标志是否设置,调用内置的 testing.Short() 函数来判断是否设置了 -short 标志。如果设置了该标志,测试函数将使用 t.Skip 函数跳过测试并返回一个消息,表示测试被跳过。
测试函数还包含了一段注释代码,用于检查名为 CI 的环境变量的值,如果环境变量被设置了,那么测试将会被跳过。这段代码可以用于在运行测试时跳过测试,例如在持续集成环境中运行测试。在检查是否需要跳过测试后,测试函数调用了 time.Sleep 函数来模拟一个耗时很长的操作,该操作需要花费一个小时才能完成。这样做是为了演示如何测试执行长时间操作的函数。
最后,测试函数调用 checker.Equal 函数来比较调用 add 函数的结果和期望的两个整数之和的结果是否相等。checker.Equal 函数是 assert 对象提供的一个方便函数,用于检查两个值是否相等,如果它们不相等,则会报告一个错误。
性能测试
func fib(n int) int {
if n == 0 || n == 1 {
return n
}
return fib(n-2) + fib(n-1)
}
// 注意 go test 默认不运行 benchmark 测试,需要传入 -bench 参数
// go test -bench .
// go test -bench='fib$' .
// 指定运行的时间
// go test -bench='fib$' -benchtime=10s .
// 指定运行次数
// go test -bench='fib$' -benchtime=30x .
// 指定运行的轮数
// go test -bench='fib$' -benchtime=10s -count=5 .
func Benchmark_fib(b *testing.B) {
// b.N 是测试运行的次数,go test 会自动侦测需要运行的时间,保证有足够的次数
for n := 0; n < b.N; n++ {
fib(20)
}
}
程序定义了一个名为 fib 的函数,用于计算斐波那契数列中第 n 项的值。如果 n 等于 0 或 1,则返回 n,否则递归地计算 fib(n-2) 和 fib(n-1) 的和并返回结果。
代码中还包含一个名为 Benchmark_fib 的基准测试函数,用于测试计算斐波那契数列的效率。测试函数使用了 Go 语言标准库的 testing 包进行基准测试。它使用了 b.N 变量来控制测试运行的次数,这个变量由 testing 包自动设置,并根据需要调整以确保测试有足够的时间来运行。
在测试函数中,循环运行 fib(20) 函数,以测试计算斐波那契数列的效率。测试函数不会直接输出任何结果,而是使用 go test 命令来运行测试,并输出测试结果。
通过运行 go test -bench 命令,可以运行基准测试,并输出测试结果。默认情况下,go test 不会运行基准测试,因此需要传入 -bench 参数来指定运行基准测试。可以使用 .(点) 来指定运行所有基准测试函数,也可以使用正则表达式来指定只运行名称匹配的基准测试函数。例如,go test -bench='fib$' 命令将只运行名称以 fib 结尾的基准测试函数。
可以使用 -benchtime 参数来指定基准测试运行的时间。例如,go test -bench='fib$' -benchtime=10s 命令将运行名称以 fib 结尾的基准测试函数,每次运行持续 10 秒钟。可以使用 -benchtime 参数的值来控制测试的精度。
可以使用 -count 参数来指定运行基准测试的次数。例如,go test -bench='fib$' -benchtime=30x 命令将运行名称以 fib 结尾的基准测试函数,每次运行持续足够的时间来运行 30 次。可以使用 -count 参数的值来控制测试的稳定性。
TestMain 函数
// 在下面所有测试方法执行开始前先执行
func TestMain(m *testing.M) {
// 初始化资源
fmt.Println("initial...")
// 运行 go 的测试,相当于调用 main 方法
result := m.Run()
// 清理资源
fmt.Println("dispose...")
//退出程序
os.Exit(result)
}
/*
initial...
=== RUN Test_someFunc1
这里是正儿八经的测试...1
--- PASS: Test_someFunc1 (0.00s)
=== RUN Test_someFunc2
这里是正儿八经的测试...2
--- PASS: Test_someFunc2 (0.00s)
dispose...
*/
// 单元测试
func Test_someFunc1(t *testing.T) {
fmt.Println("这里是正儿八经的测试...1")
}
func Test_someFunc2(t *testing.T) {
fmt.Println("这里是正儿八经的测试...2")
}
代码展示了如何使用 TestMain 函数在测试执行前和执行后初始化和清理资源。 TestMain 函数必须是签名为 func TestMain(m *testing.M) 的函数,其中 m *testing.M 是 testing 包提供的管理测试执行的对象。
在这个例子中,TestMain 函数首先执行初始化操作,打印 "initial..." 字符串。接下来,m.Run() 函数运行所有的测试方法。最后,TestMain 函数执行清理操作,打印 "dispose..." 字符串。注意,如果测试执行失败,则清理操作将不会执行。
在 Test_someFunc1 和 Test_someFunc2 中,我们只是打印了一些字符串作为测试。这两个测试方法会在初始化和清理资源之间执行,因为它们是测试集合中的两个单元测试。
这个程序展示了如何在单元测试中使用 TestMain 函数来管理测试执行前和执行后的资源初始化和清理。
手动实现 setup/tearDown
func setupTest() (*sql.DB, func()) {
fmt.Println("initial resources firstly...")
// such as:
// create db resource
// create httptest.Recrorder/Request
// create gin test context
// create some seeds data
db, _ := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test")
return db, func() {
fmt.Println("clean resources finally...")
db.Close()
}
}
func Test_setupTearDown(t *testing.T) {
checker := assert.New(t)
// setup depends
db, tearDown := setupTest()
// clean resources
defer tearDown()
var count int
err := db.QueryRow("SELECT 1 AS count").Scan(&count)
checker.NoError(err)
checker.Equal(1, count)
fmt.Println("tests passed...")
/*
=== RUN Test_setupTearDown
initial resources firstly...
tests passed...
clean resources finally...
--- PASS: Test_setupTearDown (0.00s)
*/
}
程序包含一个名为setupTest()的函数,用于初始化测试所需的资源,具体来说,该函数打开一个名为test的mysql数据库,并返回一个数据库连接对象以及一个用于清理资源的闭包函数。闭包函数包含了关闭数据库连接的逻辑。
接下来,有一个名为Test_setupTearDown()的测试函数。该函数利用setupTest()函数来初始化测试所需的资源。在测试完成后,使用defer关键字来延迟执行返回的tearDown()闭包函数,以确保资源被正确释放。在测试函数中,使用assert库中的NoError()和Equal()方法检查查询数据库的结果是否符合预期,以确保测试通过。最后,通过打印一条"tests passed"的成功信息来标识测试执行成功。
使用 testify suite 包装测试对象
// 要测试的对象
type person struct {
RealName string
Age int
}
// 要测试的行为
func (p *person) Grow() {
p.Age += 1
}
type personSuite struct {
suite.Suite
// 把要测试的对象放这里来
p *person
// 把测试对象的 depends 放这里来,一般是 mock 的对象
// mocked objects
}
// 在每个 test 函数之前运行
func (suite *personSuite) SetupTest() {
fmt.Println("setup test called")
suite.p = &person{Age: 20}
}
func (suite *personSuite) TestGrow() {
// 调用被测试对象的行为
suite.p.Grow()
// 验证结果
suite.Equal(21, suite.p.Age)
}
// 为了让 go test 调用到,需要一个普通的测试函数来作为入口
func TestPersonSuite(t *testing.T) {
suite.Run(t, new(personSuite))
}
程序定义了一个 person 结构体,包含真实姓名和年龄两个属性,以及 Grow() 方法,用于增加该人物对象的年龄。还定义了一个 personSuite 结构体,嵌入了 suite.Suite,用于存储要测试的对象以及测试对象的依赖关系。
personSuite 结构体实现了 SetupTest() 方法,用于在每个测试函数之前进行一些初始化工作。定义了一个 TestGrow() 测试函数,调用被测试对象的 Grow() 方法并验证其结果是否正确。
最后定义了一个普通的测试函数 TestPersonSuite(),用于作为入口调用 suite.Run() 函数来运行所有测试函数。
使用 testify 实现 setup/tearDown
SetupSuite -> SetupTest -> TearDownTest -> TearDownSuite
// 要测试的对象
type person struct {
RealName string
Age int
}
// 要测试的行为
func (p *person) Grow() {
p.Age += 1
}
type testCase struct {
Name string
Run func()
}
type personSuite struct {
suite.Suite
p *person
}
// 只运行一次,在整个 suite 之前
func (suite *personSuite) SetupSuite() {
log.Println("before suite called")
}
// 每一个 test 都会调用
func (suite *personSuite) SetupTest() {
log.Println("setup test called")
suite.p = &person{Age: 20}
}
func (suite *personSuite) TearDownTest() {
log.Println("teardown test called")
}
// 只运行一次,在整个 suite 之后
func (suite *personSuite) TearDownSuite() {
log.Println("teardown suite called")
}
func (suite *personSuite) TestGrow() {
suite.p.Grow()
suite.Equal(21, suite.p.Age)
/*
2009/11/10 23:00:00 before suite called
[T+0001ms]
=== RUN TestPersonSuite/TestGrow
[T+0001ms]
2009/11/10 23:00:00 setup test called
2009/11/10 23:00:00 teardown test called
2009/11/10 23:00:00 teardown suite called
[T+0001ms]
*/
}
func TestPersonSuite(t *testing.T) {
suite.Run(t, new(personSuite))
}
在 personSuite 结构体类型中,SetupSuite() 方法在整个 suite 运行之前运行一次,TearDownSuite() 方法在整个 suite 运行之后运行一次,SetupTest() 方法在每个 test 运行之前运行一次,TearDownTest() 方法在每个 test 运行之后运行一次。
TestGrow() 方法作为测试用例,调用 person 结构体类型的 Grow() 方法,并通过 suite.Equal() 方法来判断结果是否符合预期,TestPersonSuite() 函数是入口函数,通过 suite.Run() 函数来启动测试。
表格驱动测试中手动调用 setupTest
// 要测试的对象
type person struct {
RealName string
Age int
}
func (p *person) Say() string {
return "hello, " + p.RealName
}
type testCase struct {
Name string
Run func()
}
type personSuite struct {
suite.Suite
p *person
}
// 每一个 test 都会调用
func (suite *personSuite) SetupTest() {
log.Println("setup test called")
suite.p = &person{Age: 20}
}
func (suite *personSuite) TearDownTest() {
log.Println("teardown test called")
}
func (suite *personSuite) TestSay() {
testCases := []testCase{
{
Name: "hi zhangsan",
Run: func() {
suite.p.RealName = "zhangsan"
suite.Equal("hello, zhangsan", suite.p.Say())
suite.Equal(20, suite.p.Age)
},
},
{
Name: "hi lisi",
Run: func() {
suite.p.RealName = "lisi"
suite.Equal("hello, lisi", suite.p.Say())
suite.Equal(20, suite.p.Age)
},
},
}
for _, testCase := range testCases {
// 这里写了会被调用 3 次(两个 it),不写只会被调用 1 次
suite.SetupTest()
suite.Run(testCase.Name, testCase.Run)
// 这里写了会被调用 3 次(两个 it),不写只会被调用 1 次
suite.TearDownTest()
}
/*
=== RUN TestPersonSuite
=== RUN TestPersonSuite/TestSay
[T+0000ms]
2009/11/10 23:00:00 setup test called
2009/11/10 23:00:00 setup test called
[T+0001ms]
=== RUN TestPersonSuite/TestSay/hi_zhangsan
[T+0001ms]
2009/11/10 23:00:00 teardown test called
2009/11/10 23:00:00 setup test called
[T+0001ms]
=== RUN TestPersonSuite/TestSay/hi_lisi
[T+0001ms]
2009/11/10 23:00:00 teardown test called
2009/11/10 23:00:00 teardown test called
[T+0001ms]
--- PASS: TestPersonSuite (0.00s)
--- PASS: TestPersonSuite/TestSay (0.00s)
--- PASS: TestPersonSuite/TestSay/hi_zhangsan (0.00s)
--- PASS: TestPersonSuite/TestSay/hi_lisi (0.00s)
PASS
*/
}
func TestPersonSuite(t *testing.T) {
suite.Run(t, new(personSuite))
}
程序在 TestSay 中定义了两个子测试用例 hi zhangsan 和 hi lisi。我们通过 for 循环遍历这两个子测试用例,并在每个子测试用例之前和之后执行 SetupTest() 和 TearDownTest() 函数。这样做的目的是在每个子测试用例之前和之后重新初始化 person 对象,以确保每个测试用例都是独立的。
在运行测试用例时,可以看到输出了三次 setup test called 和三次 teardown test called,这是因为在 for 循环中调用了 SetupTest() 和 TearDownTest() 函数。
可以看到,在运行 hi_zhangsan 子测试用例之前,先调用了 teardown test called 函数清理上一个子测试用例的状态,然后再调用 setup test called 函数重新初始化 person 对象。
慎用 testify 的 beforeTest/afterTest
suite 的 BeforeTest 运行在 setupTest 之后,AfterTest 运行在 TearDown 之前,但是通常不太需要这样的钩子行为,而且用在表格驱动测试中,钩子行为显得有点混乱,不推荐使用。
// 要测试的对象
type person struct {
RealName string
Age int
}
// 要测试的行为
func (p *person) Grow() {
p.Age += 1
}
func (p *person) Say() string {
return "hello, " + p.RealName
}
type testCase struct {
Name string
Run func()
}
type personSuite struct {
suite.Suite
p *person
}
func (suite *personSuite) SetupSuite() {
log.Println("before suite called")
}
func (suite *personSuite) SetupTest() {
log.Println("setup test called")
suite.p = &person{Age: 20}
}
func (suite *personSuite) TearDownTest() {
log.Println("teardown test called")
}
func (suite *personSuite) TearDownSuite() {
log.Println("teardown suite called")
}
func (suite *personSuite) BeforeTest(suiteName, testName string) {
// before test => personSuite TestGrow
log.Println("before test =>", suiteName, testName)
}
func (suite *personSuite) AfterTest(suiteName, testName string) {
// after test => personSuite TestGrow
log.Println("after test =>", suiteName, testName)
}
func (suite *personSuite) TestGrow() {
suite.p.Grow()
suite.Equal(21, suite.p.Age)
/*
=== RUN TestPersonSuite
[T+0000ms]
2009/11/10 23:00:00 before suite called
[T+0001ms]
=== RUN TestPersonSuite/TestGrow
[T+0001ms]
2009/11/10 23:00:00 setup test called
2009/11/10 23:00:00 before test => personSuite TestGrow
2009/11/10 23:00:00 after test => personSuite TestGrow
2009/11/10 23:00:00 teardown test called
2009/11/10 23:00:00 teardown suite called
[T+0001ms]
--- PASS: TestPersonSuite (0.00s)
--- PASS: TestPersonSuite/TestGrow (0.00s)
PASS
*/
}
func TestPersonSuite(t *testing.T) {
suite.Run(t, new(personSuite))
}
在 SetupTest() 中,我们定义了在每个测试之前需要运行的代码,以确保每个测试开始时都有一个干净的状态。在 TearDownTest() 中,我们定义了在每个测试完成之后需要运行的代码。在 TearDownSuite() 中,我们定义了在所有测试完成之后需要运行的代码。
在 BeforeTest() 中,我们定义了在每个测试之前需要运行的代码。在这个例子中,我们同样使用 log.Println() 打印了当前测试套件名称和测试名称。在 AfterTest() 中,我们定义了在每个测试完成之后需要运行的代码。同样用 log.Println() 打印了当前测试套件名称和测试名称。
在 TestGrow() 中,我们定义了一个测试函数,用于测试 person.Grow() 方法。在测试函数中,我们调用 suite.p.Grow() 方法,将 person.Age 增加了 1,然后使用 suite.Equal() 函数检查是否正确地将 person.Age 增加了 1。
使用 testify 适度的 mock 对象
type base interface {
GetBase(int) (int, error)
}
// 实现一个被依赖的对象
type mockBase struct {
mock.Mock
}
type person struct {
// person 依赖 base 模块(注意是接口对象)
Base base
RealName string
Age int
}
func (p *person) Salary() (int, error) {
base, err := p.Base.GetBase(p.Age)
return 100 + base, err
}
// 真正的 base 模块还没实现
// 或者为了单测,不打算受到 base 模块实现的影响
// 实现了 base 接口
func (m *mockBase) GetBase(age int) (int, error) {
// 记录调用的次数
args := m.Called(age)
// 提供期望的结果
return args.Int(0), args.Error(1)
}
type personSuite struct {
suite.Suite
mockedBase *mockBase
p *person
}
func (suite *personSuite) SetupTest() {
suite.mockedBase = &mockBase{}
suite.p = &person{RealName: "zhangsan", Age: 20, Base: suite.mockedBase}
}
func (suite *personSuite) TestGrow() {
suite.mockedBase.On("GetBase", suite.p.Age).Return(5, nil)
// 不想限定参数,可以使用 mock.Anything
// suite.mockedBase.On("GetBase", mock.Anything).Return(5, nil)
salary, err := suite.p.Salary()
suite.NoError(err)
suite.Equal(105, salary)
}
func TestPersonSuite(t *testing.T) {
suite.Run(t, new(personSuite))
}
程序定义了一个接口 base,并在 person 中将 base 作为一个依赖。person 的 Salary 方法依赖于 base 模块中的 GetBase 方法,将 GetBase 方法的返回值加上 100 后,作为 Salary 方法的返回值。
为了独立测试 person 的 Salary 方法,实现了 mockBase 结构体,它实现了 base 接口中的 GetBase 方法,并且可以记录 GetBase 方法的调用次数和返回期望的结果。
在 personSuite 中,通过 SetupTest 方法初始化了一个 mockBase 实例和一个 person 实例,并将 mockBase 作为 person 的依赖传入。
在 TestGrow 方法中,使用 mockedBase.On 方法来设置 GetBase 方法的期望调用,并返回预期的结果。然后调用 person 的 Salary 方法,检查返回值是否符合预期。
对网络请求进行 mock 操作
有时候不想 mock 整个对象,或者被依赖的对象不是接口定义的,涉及到外部网络的时候可以使用 gock 这个库来 mock http 请求:
type person struct {
RealName string
Age int
}
func (p *person) getBase() (int, error) {
// 根本不存在的一个 server 端点
URL := fmt.Sprintf("http://config.api.com/base/config?age=%d", p.Age)
res, err := http.Get(URL)
if err != nil {
return 0, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return 0, err
}
config := struct {
Base int `json:"base`
}{}
err = json.Unmarshal(body, &config)
return config.Base, err
}
func (p *person) Salary() (int, error) {
base, err := p.getBase()
return 100 + base, err
}
type personSuite struct {
suite.Suite
p *person
}
func (suite *personSuite) SetupTest() {
suite.p = &person{RealName: "zhangsan", Age: 20}
}
func (suite *personSuite) TestSalary() {
defer gock.Off()
// 拦截外部请求,让其直接返回期望的结果
gock.New(fmt.Sprintf("http://config.api.com/base/config?age=%d", suite.p.Age)).
Reply(200).
JSON(map[string]int{"base": 5})
salary, err := suite.p.Salary()
suite.NoError(err)
suite.Equal(105, salary)
// 验证没有执行的 mock 操作了
suite.True(gock.IsDone())
}
func TestPersonSuite(t *testing.T) {
suite.Run(t, new(personSuite))
}
程序主要测试了一个 person 结构体的 Salary() 方法。在 Salary() 方法中,会调用 getBase() 方法获取基础工资,而 getBase() 方法会通过 HTTP 请求从 http://config.api.com/base/config 接口获取基础工资。为了在测试时避免依赖外部接口,代码使用了 gock 包模拟了这个 HTTP 请求,将其直接返回期望的结果,从而避免了真实的网络请求。这个测试用例的目的是验证 Salary() 方法能否正确计算出工资,以及在模拟 HTTP 请求的情况下,是否能正常工作。
禁止并行测试
go test 默认在每个 pkg 是串行的除非 test 文件使用了 t.Paranell(),但是在各个 pkg 之间是并行的,有时候需要禁用:
go test -v -p 1
启用并行测试
程序在测试函数中使用 t.Parallel 函数,以便并行运行测试子集,提高测试效率。
func sum(x, y int) int {
return x + y
}
func Test_sum(t *testing.T) {
checker := assert.New(t)
// 并行
t.Parallel()
var testcases = []struct {
x int
y int
expected int
}{
{1, 1, 2},
{2, 2, 4},
{2, 5, 7},
}
for _, tc := range testcases {
t.Run(fmt.Sprintf("sum(%d+%d)", tc.x, tc.y), func(t *testing.T) {
t.Parallel()
got := sum(tc.x, tc.y)
checker.Equal(tc.expected, got)
})
}
}
查看测试覆盖率
# 显示覆盖率报告
go test -cover
# 生成覆盖率文件
go test -cover -coverprofile=coverage.out
# 查看每个函数的覆盖率
go tool cover -func=coverage.out
# 生成 html 网页
go tool cover -html=coverage.out