15.单元测试
# 01.单元测试
# 0、单元测试规范
- 单元测试文件名命名规范为
example_test.go
。 - 测试用例的函数名称必须以
Test
开头,例如TestExample
。 - 如果存在
func Foo
,单测函数可以带下划线,为func Test_Foo
。 - 如果存在
func (b *Bar) Foo
,单测函数可以为func TestBar_Foo
。 - 单测文件行数限制是普通文件的2倍,即
1600行
。单测函数行数限制也是普通函数的2倍,即为160行
。 - 圈复杂度、列数限制、 import 分组等其他规范细节和普通文件保持一致。
- 由于单测文件内的函数都是不对外的,所有可导出函数可以没有注释,但是结构体定义时尽量不要导出。
- 每个重要的可导出函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试。
# 1、普通方法单元测试
Go 语言推荐测试文件和源代码文件放在一块,测试文件以
_test.go
结尾比如,当前 package 有
calc.go
一个文件,我们想测试calc.go
中的Add
和Mul
函数那么应该新建
calc_test.go
作为测试文件文件结构
example/
|--calc.go
|--calc_test.go
1
2
3
2
3
calc.go
package main
func Add(a int, b int) int {
return a + b
}
func Mul(a int, b int) int {
return a * b
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
calc_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
if ans := Add(1, 2); ans != 3 {
t.Errorf("1 + 2 expected be 3, but %d got", ans)
}
if ans := Add(-10, -20); ans != -30 {
t.Errorf("-10 + -20 expected be -30, but %d got", ans)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
go test -v
,-v
参数会显示每个用例的测试结果,另外-cover
参数可以查看覆盖率- 可以用
-run
参数指定,该参数支持通配符*
,和部分正则表达式,例如^
、$
- 可以用
$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok days-test/example 0.006s
1
2
3
4
5
2
3
4
5
# 2、结构体方法单元测试
- bar.go
package main
type Bar struct {
}
func (b *Bar) Foo() string {
return "Hello, World!"
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- bar_test.go
package main
import (
"testing"
)
func TestBar_Foo(t *testing.T) {
// 创建 Bar 实例
b := &Bar{}
// 调用方法
result := b.Foo()
// 预期结果
expected := "Hello, World!"
// 检查结果是否符合预期
if result != expected {
t.Errorf("Got %s, expected %s", result, expected)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3、子测试(Subtests)
- 子测试是 Go 语言内置支持的,可以在某个测试用例中,根据测试场景使用
t.Run
创建不同的子测试用例 - 之前的例子测试失败时使用
t.Error/t.Errorf
,这个例子中使用t.Fatal/t.Fatalf
- 区别在于前者遇错不停,还会继续执行其他的测试用例,后者遇错即停
# 1、一般子测试
package main
import "testing"
func TestMul(t *testing.T) {
t.Run("pos", func(t *testing.T) {
if Mul(2, 3) != 6 {
t.Fatal("fail")
}
})
t.Run("neg", func(t *testing.T) {
if Mul(2, -3) != -6 {
t.Fatal("fail")
}
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 运行某个测试用例的子测试
$ go test -run TestMul/pos -v
=== RUN TestMul
=== RUN TestMul/pos
--- PASS: TestMul (0.00s)
--- PASS: TestMul/pos (0.00s)
PASS
ok days-test/example 0.005s
1
2
3
4
5
6
7
2
3
4
5
6
7
# 2、推荐写法
- 新增用例非常简单,只需给 cases 新增一条测试数据即可。
- 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。
- 用例失败时,报错信息的格式比较统一,测试报告易于阅读。
package main
import "testing"
func TestMul(t *testing.T) {
cases := []struct {
Name string
A, B, Expected int
}{
{"pos", 2, 3, 6},
{"neg", 2, -3, -6},
{"zero", 2, 0, 0},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
if ans := Mul(c.A, c.B); ans != c.Expected {
t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)
}
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 4、setup 和 teardown
- 如果在同一个测试文件中,每一个测试用例运行前后的逻辑是相同的,一般会写在 setup 和 teardown 函数中
- 在这个测试文件中,包含有2个测试用例,
Test1
和Test2
。 - 如果测试文件中包含函数
TestMain
,那么生成的测试将调用 TestMain(m),而不是直接运行测试。 - 调用
m.Run()
触发所有测试用例的执行,并使用os.Exit()
处理返回的状态码,如果不为0,说明有用例失败。 - 因此可以在调用
m.Run()
前后做一些额外的准备(setup)和回收(teardown)工作。
package main
import (
"fmt"
"os"
"testing"
)
func setup() {
fmt.Println("Before all tests")
}
func teardown() {
fmt.Println("After all tests")
}
func Test1(t *testing.T) {
fmt.Println("I'm test1")
}
func Test2(t *testing.T) {
fmt.Println("I'm test2")
}
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
上次更新: 2024/3/13 15:35:10