返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

12.1 单元测试

发布于 2024-10-12 19:15:50 字数 7854 浏览 0 评论 0 收藏 0

为测试非导出成员,测试文件也放在目标包内。

  • 测试文件以 _test.go 结尾。

  • 通常与测试目标主文件名相同,如 sort_test.go
  • 构建命令( go build )忽略测试文件。
  • 测试命令( go test ):

  • 忽略以 _. 开头的文件。
  • 忽略 testdata 子目录。
  • 执行 go vet 检查。
  • 测试函数名 Test<Name>

  • Test 为识别标记。
  • <Name> 为测试名称,首字母大写。如: TestSort
  • 测试函数内以 ErrorFail 等方法标记失败。

  • Fail :失败,继续当前函数。
  • FailNow : 失败,终止当前函数。
  • SkipNow : 跳过,终止当前函数。
  • Log : 输出信息,仅失败或 -v 时有效。
  • Error : Fail + Log
  • Fatal : FailNow + Log
  • SkipSkipNow + Log
  • os.Exit :失败,测试进程终止。
// main_test.go

package main

import (
    "testing"
)

func TestAdd(t *testing.T) {
    z := add(1, 2)
    if z != 3 { t.FailNow() }
}
$ go test -v

=== RUN   TestAdd
--- PASS: TestAdd (0.00s)

PASS
ok  	test	0.004s

模式

  • 本地模式(local directory mode): go test , go test -v
  • 不缓存测试结果。
  • 列表模式(package list mode): go test mathgo test . , go test ./...
  • 缓存结果,直接输出。避免不必要的重复运行。
  • 缓存输出有 (cached) 标记。
  • 某些参数( -count )导致缓存失效。
  • 执行 go clean -testcache 清除缓存。
$ go test .
ok  	test	(cached)

$ go test -count 2 .
ok  	test	0.004s

执行

$ go test             // 测试当前包。
$ go test math        // 测试指定包。
$ go test ./mylib     // 使用相对路径。
$ go test ./...       // 测试当前及所有子包。

执行参数:

$ go help test
$ go help testflag
  • -args : 命令行参数。(包列表必须在此参数前)
  • -c : 仅编译,不执行。(用 -o 修改默认测试可执行文件名)
  • -json : 输出 JSON 格式。
  • -count : 执行次数。(默认 1)
  • -list : 用正则表达式列出测试函数名,不执行。
  • -run :用正则表达式执行要执行的测试函数。
  • -skip : 需要跳过的测试函数。
  • -timeout : 超时 panic!(默认 10m)
  • -v : 输出详细信息。
  • -x : 输出构建信息。

参数可添加 test. 前缀,比如 -test.v ,以便区分 benchmark 参数。

$ go test -v -run "Add" -count 2 -timeout 1m20s ./...

=== RUN   TestAdd
    add_test.go:10: abc
--- PASS: TestAdd (0.00s)

=== RUN   TestAdd
    add_test.go:10: abc
--- PASS: TestAdd (0.00s)

PASS
ok  	test/mylib	0.003s

并行

默认情况下,包内串行,多包并行。

// main_test.go

func TestA(t *testing.T) {
    time.Sleep(time.Second * 10)
}

func TestB(t *testing.T) {
    time.Sleep(time.Second * 10)
}
// ./mylib/demo_test.go

func TestX(t *testing.T) {
	time.Sleep(time.Second * 10)
}
# 单个包,串行。

$ go clean -cache -testcache
$ time go test -v

=== RUN   TestA
--- PASS: TestA (10.01s)
=== RUN   TestB
--- PASS: TestB (10.01s)

PASS
ok  	test	20.022s

real	0m21.960s  <------ !!!!
user	0m1.917s
sys		0m1.381s
# 多个包,并行。

$ go clean -cache -testcache; 
$ time go test -v -run "Test[AX]" ./...

=== RUN   TestA
--- PASS: TestA (10.00s)
PASS
ok  	test	10.006s

=== RUN   TestX
--- PASS: TestX (10.00s)

PASS
ok  	test/mylib	10.006s

real	0m11.956s  <------- !!!!
user	0m1.915s
sys		0m1.587s

参数 -p, -parallel 用于设置 GOMAXPROCS ,这会影响并发执行。

$ go clean -cache -testcache
$ time go test -v -run "Test[AX]" -p 1 ./...

=== RUN   TestA
--- PASS: TestA (10.00s)
PASS
ok  	test	10.023s

=== RUN   TestX
--- PASS: TestX (10.00s)
PASS
ok  	test/mylib	10.008s

real	0m23.227s   <------ !!!!
user	0m2.371s
sys		0m2.082s

调用 t.Parallel() 后,并发测试启动后暂停( PAUSE )。

等所有串行测试结束后,并发再恢复( CONT , continue)执行。

func TestA(t *testing.T) {
    t.Parallel()                    // 通常放在第一行。
    time.Sleep(time.Second * 10)
}

func TestB(t *testing.T) {
    t.Parallel()
    time.Sleep(time.Second * 10)
}

func TestC(t *testing.T) {
    time.Sleep(time.Second * 10)
}
# AB 并行。
$ go clean -cache -testcache; go test -v -run "[AB]"

=== RUN   TestA
=== PAUSE TestA

=== RUN   TestB
=== PAUSE TestB

=== CONT  TestA
=== CONT  TestB

--- PASS: TestB (10.00s)
--- PASS: TestA (10.00s)

PASS
ok  	test	10.011s
# AB 并行,C 串行。
$ go clean -cache -testcache; go test -v

=== RUN   TestA
=== PAUSE TestA

=== RUN   TestB
=== PAUSE TestB

=== RUN   TestC
--- PASS: TestC (10.01s)  // 等待串行结束。

=== CONT  TestA           // A 恢复。(并发)
=== CONT  TestB           // B 恢复。(并发)

--- PASS: TestB (10.01s)
--- PASS: TestA (10.01s)

PASS
ok  	test	20.029s

如设置 -cpu-count 参数,那么同一测试函数依旧串行执行多次。

$ go clean -cache -testcache; go test -v -count 2 -run "TestA"

=== RUN   TestA
=== PAUSE TestA
=== CONT  TestA
--- PASS: TestA (10.01s)

=== RUN   TestA
=== PAUSE TestA
=== CONT  TestA
--- PASS: TestA (10.00s)

PASS
ok  	test	20.017s

内部实现

每个测试函数都在独立 goroutine 内运行。
正常情况下,执行器会阻塞,等待 test goroutine 结束。

// src/testing/testing.go

func runTests(...) {
    for _, test := range tests {
        t.Run(test.Name, test.F)   // 串行。
    }
}

func (t *T) Run(...) {
    go tRunner(t, f)
	if !<-t.signal { runtime.Goexit() }    
}

func tRunner(t *T, fn func(t *T)) {
    signal := true
    
	defer func() {
		t.signal <- signal
	}()
    
	defer func() {
		t.runCleanup()
	}()
    
	fn(t)    
}

调用 t.Parallel ,该方法会立即发回信号,让外部阻塞( Run )结束,继续下一个测试。
自身阻塞,等待串行测试结束后发回信息,恢复执行。

func (t *T) Parallel() {
	t.chatty.Updatef(t.name, "=== PAUSE %s\n", t.name)

	t.signal <- true   // Release calling test.
	t.context.waitParallel()

	t.chatty.Updatef(t.name, "=== CONT  %s\n", t.name)
}

助手

将调用 Helper 的函数标记为测试助手。
输出测试信息时跳过助手函数,直接显示测试函数文件名、行号。

  • 直接在测试函数中调用无效。
  • 测试助手可用作断言。
package main

import (
	"testing"
)

func assert(t *testing.T, b bool) {
	t.Helper()
	if !b { t.Fatal("assert fatal") }
}

func TestA(t *testing.T) {
	assert(t, false)
}
$ go test -v -run "A"

=== RUN   TestA

main_test.go:13: assert fatal    # 不会显示助手函数信息。

--- FAIL: TestA (0.00s)
FAIL

exit status 1
FAIL	test	0.011s

清理

为测试函数注册清理函数,在测试结束时执行。

  • 如注册多个,则按 FILO 顺序执行。
  • 即便发生 panic ,也能确保清理函数执行。
func TestA(t *testing.T) {
	t.Cleanup(func(){ println("1 cleanup.") });
	t.Cleanup(func(){ println("2 cleanup.") });
	t.Cleanup(func(){ println("3 cleanup.") });

	t.Log("body.")
}
$ go test -v -run "A"

=== RUN   TestA

body.

3 cleanup.
2 cleanup.
1 cleanup.

--- PASS: TestA (0.00s)
PASS
ok  	test	0.004s

defer 的区别:即便在其他函数内注册,也会等测试结束后再执行。

func TestA(t *testing.T) {

	func() {
		t.Cleanup(func(){ println("cleanup.") });
	}()

	func() {
		defer println("defer.")
	}()

	t.Log("body.")
}
 $ go test -v -run "A"
 
=== RUN   TestA

defer.
body.
cleanup.

--- PASS: TestA (0.00s)
PASS
ok  	test	0.008s

可用来写 Helper 函数。

func newDatabase(t *testing.T) *DB {
	t.Helper()

	d := Database.Open()
	t.Cleanup(func(){
		d.close()
	})

	return &d
}

func TestSelect(t *testing.T) {
    db = newDatabase(t)
    ...
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文