go test工具
Go语言中的测试依赖go test
命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。
go test 命令,会自动读取源码目录下面名为 *_test.go 的文件,生成并运行测试用的可执行文件。,所有以_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 为文档提供示例文档 |
go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
测试函数
测试函数的格式
每个测试函数必须导入testing
包,测试函数的基本格式(签名)如下:
func TestName(t *testing.T){
// ...
}
测试函数的名字必须以Test
开头,可选的后缀名必须以大写字母开头,举几个例子:
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }
其中参数t
用于报告测试失败和附加的日志信息。 testing.T
的拥有的方法如下:
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
测试函数示例
Go 语言推荐测试文件和源代码文件放在一块,测试文件以 _test.go
结尾。比如,当前 package 有 calc.go
一个文件,我们想测试 calc.go
中的 Add
和 Mul
函数,那么应该新建 calc_test.go
作为测试文件。
example/
|--calc.go
|--calc_test.go
// example/calc.go
package main
func Add(a int, b int) int {
return a + b
}
func Mul(a int, b int) int {
return a * b
}
在当前目录下,我们创建一个calc_test.go
的测试文件,并定义一个测试函数如下:
// example/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)
}
}
在example
包路径下,执行go test
命令,可以看到输出结果如下,或 go test -v
,-v
参数会显示每个用例的测试结果,另外 -cover
参数可以查看覆盖率。
PS C:\Users\kaikai\Desktop\golang\calc> go mod init calc
PS C:\Users\kaikai\Desktop\golang\calc> go test go test
PASS
ok calc 0.183s
PS C:\Users\kaikai\Desktop\golang\calc> go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok calc 0.419s
PS C:\Users\kaikai\Desktop\golang\calc> go test -cover
PASS
coverage: 50.0% of statements
ok calc 0.387s
如果其中有很多测试的函数,但是你只想测试其中一个,可以单独执行,例如 TestAdd
,可以用 -run
参数指定,该参数支持通配符 *
,和部分正则表达式,例如 ^
、$
。
PS C:\Users\kaikai\Desktop\golang\calc> go test -run TestAdd -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok calc 0.383s
子测试(Subtests)
子测试是 Go1.7+中新增了子测试内置支持的,可以在某个测试用例中,根据测试场景使用 t.Run
创建不同的子测试用例:
// calc_test.go
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")
}
})
}
- 之前的例子测试失败时使用
t.Error/t.Errorf
,这个例子中使用t.Fatal/t.Fatalf
,区别在于前者遇错不停,还会继续执行其他的测试用例,后者遇错即停。
此时我们再执行go test
命令就能够看到更清晰的输出内容了:
PS C:\Users\kaikai\Desktop\golang\calc> go test -run TestMul/pos -v
=== RUN TestMul
=== RUN TestMul/pos
--- PASS: TestMul (0.00s)
--- PASS: TestMul/pos (0.00s)
PASS
ok calc 0.435s
测试组
对于多个子测试的场景,推荐使用(table-driven tests)的写法:
// calc_test.go
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)
}
})
}
}
所有用例的数据组织在切片 cases
中,看起来就像一张表,借助循环创建子测试。这样写的好处有:
- 新增用例非常简单,只需给 cases 新增一条测试数据即可。
- 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。
- 用例失败时,报错信息的格式比较统一,测试报告易于阅读。
如果数据量较大,或是一些二进制数据,推荐使用相对路径从文件中读取。
测试覆盖率
测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover
来查看测试覆盖率。例如:
PS C:\Users\kaikai\Desktop\golang\calc> go test -cover
PASS
coverage: 100.0% of statements
ok calc 0.406s
从上面的结果可以看到我们的测试用例覆盖了100%的代码。
Go还提供了一个额外的-coverprofile
参数,用来将覆盖率相关的记录信息输出到一个文件。例如:
PS C:\Users\kaikai\Desktop\golang\calc>$ go test -cover -coverprofile=calc
PASS
coverage: 100.0% of statements
ok calc 0.452s
上面的命令会将覆盖率相关的信息输出到当前文件夹下面的calc文件中,然后我们执行go tool cover -html=calc
,使用cover
工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。
上图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。
基准测试
基准测试函数格式
基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:
func BenchmarkName(b *testing.B){
// ...
}
基准测试以Benchmark
为前缀,需要一个*testing.B
类型的参数b,基准测试必须要执行b.N
次,这样的测试才有对照性,b.N
的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B
拥有的方法如下:
func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()
基准测试示例
我们为calc包中的Add
函数编写基准测试如下:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
if ans := Add(1, 2); ans != 3 {
b.Errorf("1 + 2 expected be 3, but %d got", ans)
}
}
}
基准测试并不会默认执行,需要增加-bench
参数,所以我们通过执行go test -bench=Split
命令执行基准测试,输出结果如下:
PS C:\Users\kaikai\Desktop\golang\calc> $ go test -benchmemem -bench .
goos: windows
goarch: amd64 ns/op 0 B/op 0 allocs/op
pkg: calc
cpu: AMD Ryzen 7 5800U with Radeon Graphics
BenchmarkAdd-16 1000000000 0.5880 0 ns/op 0 B/op 0 allocs/op
PASS
ok calc 1.045s
其中BenchmarkAdd-16
表示对Add函数进行基准测试,数字16
表示GOMAXPROCS
的值,这个对于并发基准测试很重要。1000000000
和0.5880 0 ns/op
表示每次调用Split
函数耗时0.5880ns
,这个结果是1000000000
次调用的平均值。
我们还可以为基准测试添加-benchmem
参数,来获得内存分配的统计数据。
PS C:\Users\kaikai\Desktop\golang\calc> $ go test -benchmem -bench . -benchmemktop\golang\cale>
goos: windows
goarch: amd64
pkg: calc
cpu: AMD Ryzen 7 5800U with Radeon Graphics
BenchmarkAdd-16 1000000000 0.6435 ns/op 0 B/op 0 allocs/op
PASS
ok calc 1.124s
其中,0 B/op
表示每次操作内存分配了小于1字节,0 allocs/op
则表示每次操作进行了没有内存分配。
性能比较函数
上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差别是多少?再或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。
性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用。举个例子如下:
func benchmark(b *testing.B, size int){/* ... */}
func Benchmark10(b *testing.B){ benchmark(b, 10) }
func Benchmark100(b *testing.B){ benchmark(b, 100) }
func Benchmark1000(b *testing.B){ benchmark(b, 1000) }
例如我们编写了一个计算斐波那契数列的函数如下:
// fib.go
// Fib 是一个计算第n个斐波那契数的函数
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
我们编写的性能比较函数如下:
// fib_test.go
func benchmarkFib(b *testing.B, n int) {
for i := 0; i < b.N; i++ {
Fib(n)
}
}
func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) }
func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }
运行基准测试:
PS C:\Users\kaikai\Desktop\golang\calc> $ go test -bench=Fib
goos: windows
goarch: amd64
pkg: calc
cpu: AMD Ryzen 7 5800U with Radeon Graphics
BenchmarkFib1-16 296372254 4.055 ns/op
BenchmarkFib2-16 126167323 9.577 ns/op
BenchmarkFib3-16 78145348 15.81 ns/op
BenchmarkFib10-16 2247198 598.1 ns/op
BenchmarkFib20-16 18014 65691 ns/op
BenchmarkFib40-16 2 997968350 ns/op
PASS
ok calc 13.157s
这里需要注意的是,默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。
最终的BenchmarkFib40只运行了两次,每次运行的平均值只有不到一秒。像这种情况下我们应该可以使用-benchtime
标志增加最小基准时间,以产生更准确的结果。例如:
PS C:\Users\kaikai\Desktop\golang\calc> $ go test -bench=Fib40 -benchtime=20s
goos: windows
goarch: amd64
pkg: calc
cpu: AMD Ryzen 7 5800U with Radeon Graphics
BenchmarkFib40-16 22 1024200623 ns/op
PASS
ok calc 24.025s
这一次BenchmarkFib40
函数运行了22次,结果就会更准确一些了。
使用性能比较函数做测试的时候一个容易犯的错误就是把b.N
作为输入的大小,例如以下两个例子都是错误的示范:
// 错误示范1
func BenchmarkFibWrong(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(n)
}
}
// 错误示范2
func BenchmarkFibWrong2(b *testing.B) {
Fib(b.N)
}
重置时间
b.ResetTimer
之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作。例如:
func BenchmarkAdd(b *testing.B) {
time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
b.ResetTimer() // 重置计时器
for i := 0; i < b.N; i++ {
if ans := Add(1, 2); ans != 3 {
b.Errorf("1 + 2 expected be 3, but %d got", ans)
}
}
}
并行测试
func (b *B) RunParallel(body func(*PB))
会以并行的方式执行给定的基准测试。
RunParallel
会创建出多个goroutine
,并将b.N
分配给这些goroutine
执行, 其中goroutine
数量的默认值为GOMAXPROCS
。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel
之前调用SetParallelism
。RunParallel
通常会与-cpu
标志一同使用。
func BenchmarkAddParallel(b *testing.B) {
// b.SetParallelism(1) // 设置使用的CPU数
for i := 0; i < b.N; i++ {
if ans := Add(1, 2); ans != 3 {
b.Errorf("1 + 2 expected be 3, but %d got", ans)
}
}
}
还可以通过在测试命令后添加-cpu
参数如go test -bench=. -cpu 1
来指定使用的CPU数量。
Setup与TearDown
如果在同一个测试文件中,每一个测试用例运行前后的逻辑是相同的,一般会写在 setup 和 teardown 函数中。例如执行前需要实例化待测试的对象,如果这个对象比较复杂,很适合将这一部分逻辑提取出来;执行后,可能会做一些资源回收类的工作,例如关闭网络连接,释放文件等。标准库 testing
提供了这样的机制:测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。
通过在*_test.go
文件中定义TestMain
函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。
一个使用TestMain
来设置Setup和TearDown的示例如下:
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)
}
- 在这个测试文件中,包含有2个测试用例,
Test1
和Test2
。 - 如果测试文件中包含函数
TestMain
,那么生成的测试将调用 TestMain(m),而不是直接运行测试。 - 调用
m.Run()
触发所有测试用例的执行,并使用os.Exit()
处理返回的状态码,如果不为0,说明有用例失败。 - 因此可以在调用
m.Run()
前后做一些额外的准备(setup)和回收(teardown)工作。
执行 go test
,将会输出
PS C:\Users\kaikai\Desktop\golang\calc> $go test
Before all tests
I'm test1
I'm test2
PASS
After all tests
ok example 0.006s
网络测试(Network)
TCP/HTTP
假设需要测试某个 API 接口的 handler 能够正常工作,例如 helloHandler
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
}
// test code
import (
"io/ioutil"
"net"
"net/http"
"testing"
)
func handleError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal("failed", err)
}
}
func TestConn(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
handleError(t, err)
defer ln.Close()
http.HandleFunc("/hello", helloHandler)
go http.Serve(ln, nil)
resp, err := http.Get("http://" + ln.Addr().String() + "/hello")
handleError(t, err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
handleError(t, err)
if string(body) != "hello world" {
t.Fatal("expected hello world, but got", string(body))
}
}
net.Listen("tcp", "127.0.0.1:0")
:监听一个未被占用的端口,并返回 Listener。- 调用
http.Serve(ln, nil)
启动 http 服务。 - 使用
http.Get
发起一个 Get 请求,检查返回值是否正确。 - 尽量不对
http
和net
库使用 mock,这样可以覆盖较为真实的场景。
httptest
针对 http 开发的场景,使用标准库 net/http/httptest
进行测试更为高效。
// test code
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
func TestConn(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
w := httptest.NewRecorder()
helloHandler(w, req)
bytes, _ := ioutil.ReadAll(w.Result().Body)
if string(bytes) != "hello world" {
t.Fatal("expected hello world, but got", string(bytes))
}
}
使用 httptest 模拟请求对象(req)和响应对象(w),达到了相同的目的。