11.1 单元测试
单元测试(unit test)除用来测试逻辑算法是否符合预期外,还承担着监控代码质量的责任。任何时候都可用简单的命令来验证全部功能,找出未完成任务(验收)和任何因修改而造成的错误。它与性能测试、代码覆盖率等一起保障了代码总是在可控范围内,这远比形式化的人工检查有用的多。
单元测试并非要取代人工代码审核,实际上它也无法切入到代码实现层面。但可通过测试结果为审查提供筛选依据,避免因烦琐导致代码审查沦为形式主义。单元测试可自动化进行,能持之以恒。但测试毕竟只是手段,而非目的,如何合理安排测试就需要开发人员呢因地制宜。
testing
工具链和标准库自带单元测试框架,这让测试工作变得相对容易。
- 测试代码须放在当前包以“_test.go”结尾的文件中。
- 测试函数以Test为名称前缀
- 测试命令go test 忽略以“_”或“.”开头的测试文件
- 正常编译操作会忽略测试文件。
main_test.go
package main
import "testing"
func add(x, y int) int {
return x + y
}
func TestAdd(t *testing.T){
if add(1,2) != 3{
t.FailNow()
}
}
结果
PS D:\go_learn\5\test> go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok _/D_/go_learn/5/test 0.189s
标准库testing提供了专用类型T来控制测试结果和行为
方法 | 说明 | 相关 |
---|---|---|
Fail | 失败:继续执行当前函数 | |
FailNow | 失败:立即终止终止当前测试函数 | Faild |
SkipNow | 跳过:停止执行当前函数 | Skip,Skipf,Skipped |
Log | 输出错误信息。仅失败或-v时输出 | Logf |
Parallel | 与同样设置的测试函数并行执行 | |
Error | Fail + Log | Errorf |
Fatal | FailNow+Log | Fatalf |
使用Parallel可有效利用多核优势,缩短测试时间。
func TestAdd(t *testing.T) {
t.Parallel()
t.Fail()
time.Sleep(time.Second * 2)
}
func TestAdd2(t *testing.T) {
t.Parallel()
time.Sleep(time.Second * 2)
}
结果
PS D:\go_learn\5\test> go test -v -args "b"
=== RUN TestAdd
=== PAUSE TestAdd
=== RUN TestAdd2
=== PAUSE TestAdd2
=== CONT TestAdd
=== CONT TestAdd2
--- PASS: TestAdd2 (2.00s)
--- FAIL: TestAdd (2.00s)
FAIL
exit status 1
FAIL _/D_/go_learn/5/test 2.210s
PS D:\go_learn\5\test>
从测试总耗时可以看出并行执行的结果只有2s
只有一个测试函数调用Parallel方法并没有效果,且go test 执行参数patallel必须大于1
常用测试参数
参数 | 说明 | 示例 |
---|---|---|
-args | 命令行参数 | |
-v | 输出详细信息 | |
-parallel | 并发执行,默认值为GOMAXPROCS | -parallel 2 |
-run | 指定测试函数,正则表达式 | -run “Add" |
-timeout | 全部测试累计时间超时将引发panic,默认值为10ms | -timeout 1m30s |
-count | 重复测试次数,默认值为1 |
table driven
单元测试代码一样要写得简洁优雅,好多时候,我们可以用一种类似数据表的模式来批量输入条件并依次比对结果。
func TestAdd(t *testing.T) {
var tests = []struct {
x int
y int
except int
}{
{1, 1, 2},
{2, 2, 4},
{2, 2, 5},
}
for _, tt := range tests {
actual := add(tt.x, tt.y)
if actual != tt.except {
t.Errorf("add(%d,%d):except %d,actual %d", tt.x, tt.y, tt.except, actual)
}
}
}
结果
PS D:\go_learn\5\test> go test -v -args "b"
=== RUN TestAdd
--- FAIL: TestAdd (0.00s)
main_test.go:27: add(2,2):except 5,actual 4
FAIL
exit status 1
FAIL _/D_/go_learn/5/test 0.194s
这种方式将测试数据和测试逻辑分离,更便于维护。另外,使用Error是为了让整个表全部完成测试,以便知道具体是哪组条件出现问题。
test main
某些时候,须为测试用例提供初始化和清理操作,但testing并没有setup/teardown机制。解决方法是自定义一个名为TestMain的函数,go test 会改为执行该函数,而不再是具体的测试用例。
func TestMain(m *testing.M) {
println("start testing")
code := m.Run() // 调用测试用例函数
println("exit.")
os.Exit(code)
}
example
例代码最大的用途不是测试,而是导入GoDoc等工具生成的帮助文档中。它通过对比输出(stdout)结果和内部output注释是否一致判断是否成功。
func ExampleAdd() {
fmt.Println(add(1, 2))
fmt.Println(add(2, 2))
// Output:
// 3
// 4
}
结果
=== RUN ExampleAdd
--- PASS: ExampleAdd (0.00s)
FAIL
exit status 1
FAIL _/D_/go_learn/5/test 0.283s
如果没有output注释,该示例函数就不会被执行。另外,不能使用内置函数print/println,因为它们输出到stderr。
11.2 性能测试。
性能测试函数以Benchmark为名称前缀,同样保存在“*_test.go”文件里。
func add(x, y int) int {
return x + y
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = add(1, 2)
}
}
结果
PS D:\go_learn\5\test> go test -bench .
goos: windows
goarch: amd64
BenchmarkAdd-4 2000000000 0.34 ns/op
PASS
ok _/D_/go_learn/5/test 0.898s
测试工具默认不会执行性能测试,须使用bench参数。它通过逐步调整B.N值,反复执行测试函数,直到获得准确的测量结果。
func BenchmarkAdd(b *testing.B) {
println("B.N=",b.N)
for i := 0; i < b.N; i++ {
_ = add(1, 2)
}
}
结果
PS D:\go_learn\5\test> go test -bench .
B.N= 1
goos: windows
goarch: amd64
BenchmarkAdd-4 B.N= 100
B.N= 10000
B.N= 1000000
B.N= 100000000
B.N= 2000000000
2000000000 0.35 ns/op
PASS
ok _/D_/go_learn/5/test 0.933s
如果仅希望执行性能测试,那么可以用run=NONE 忽略所有单元测试用例。
默认就以并发方式执行测试,但可用cpu参数设定多个并发限制来观察结果。
PS D:\go_learn\5\test> go test -bench . -cpu 1,2,4
B.N= 1
goos: windows
goarch: amd64
BenchmarkAdd B.N= 100
B.N= 10000
B.N= 1000000
B.N= 100000000
B.N= 2000000000
2000000000 0.34 ns/op
BenchmarkAdd-2 B.N= 1
B.N= 100
B.N= 10000
B.N= 1000000
B.N= 100000000
B.N= 2000000000
2000000000 0.34 ns/op
BenchmarkAdd-4 B.N= 1
B.N= 100
B.N= 10000
B.N= 1000000
B.N= 100000000
B.N= 2000000000
2000000000 0.38 ns/op
PASS
ok _/D_/go_learn/5/test 2.413s
某些耗时的目标,默认循环次数过少,取平均值不足以准确计量性能。可用benchtime设定最小测试时间来增加循环次数,以便返回更准确的结果;
func sleep() {
time.Sleep(time.Second)
}
func BenchmarkAdd(b *testing.B) {
println("b.N",b.N)
for i:=0;i<b.N;i++{
sleep()
}
}
结果
PS D:\go_learn\5\test> go test -bench . -benchtime 30s
b.N 1
goos: windows
goarch: amd64
BenchmarkAdd-4 b.N 50
50 1003281622 ns/op
PASS
ok _/D_/go_learn/5/test 51.370s
timer
如果要在测试函数中执行一些额外操作,那么应该阻止计时器工作。
func BenchmarkAdd(b *testing.B) {
time.Sleep(time.Second)
b.ResetTimer()
for i:=0;i<b.N;i++{
_ = add(1,2)
if i == 1{
b.StopTimer()
time.Sleep(time.Second)
b.StartTimer()
}
}
}
结果
PS D:\go_learn\5\test> go test -bench .
goos: windows
goarch: amd64
BenchmarkAdd-4 2000000000 0.70 ns/op
PASS
ok _/D_/go_learn/5/test 12.772s
memorry
性能测试关心的不仅仅是执行时间,还包括在堆上的内存分配,因为内存分配和垃圾回收的相操作也应计入消耗成本。
func heap() []byte {
return make([]byte, 1024*10)
}
func Benchmarkhead(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = heap()
}
}