1、Go语言单元测试
Go语言中的测试依赖 go test 命令,go test 命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录
内,所有以 _test.go
为后缀名的源代码文件都是 go test 测试的一部分,不会被 go build 编译到最终的可执行
文件中。
在 *_test.go
文件中有三种类型的函数,单元测试函数
、基准测试函数
和 示例函数
:
类型 | 格式 | 作用 |
---|---|---|
单元测试函数 | 前缀为Test | 测试程序的逻辑是否正确 |
基准测试函数 | 前缀为Benchmark | 测试程序的性能 |
示例函数 | 前缀为Example | 为程序提供示例 |
go test命令会遍历所有的 *_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相
应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
可以使用 go help testfunc
命令查看函数的写法。
$ go help testfunc
The 'go test' command expects to find test, benchmark, and example functions
in the "*_test.go" files corresponding to the package under test.
A test function is one named TestXxx (where Xxx does not start with a
lower case letter) and should have the signature,
func TestXxx(t *testing.T) { ... }
A benchmark function is one named BenchmarkXxx and should have the signature,
func BenchmarkXxx(b *testing.B) { ... }
A fuzz test is one named FuzzXxx and should have the signature,
func FuzzXxx(f *testing.F) { ... }
An example function is similar to a test function but, instead of using
*testing.T to report success or failure, prints output to os.Stdout.
If the last comment in the function starts with "Output:" then the output
is compared exactly against the comment (see examples below). If the last
comment begins with "Unordered output:" then the output is compared to the
comment, however the order of the lines is ignored. An example with no such
comment is compiled but not executed. An example with no text after
"Output:" is compiled, executed, and expected to produce no output.
Godoc displays the body of ExampleXxx to demonstrate the use
of the function, constant, or variable Xxx. An example of a method M with
receiver type T or *T is named ExampleT_M. There may be multiple examples
for a given function, constant, or variable, distinguished by a trailing _xxx,
where xxx is a suffix not beginning with an upper case letter.
Here is an example of an example:
func ExamplePrintln() {
Println("The output of\nthis example.")
// Output: The output of
// this example.
}
Here is another example where the ordering of the output is ignored:
func ExamplePerm() {
for _, value := range Perm(4) {
fmt.Println(value)
}
// Unordered output: 4
// 2
// 1
// 3
// 0
}
The entire test file is presented as the example when it contains a single
example function, at least one other function, type, variable, or constant
declaration, and no tests, benchmarks, or fuzz tests.
See the documentation of the testing package for more information.
通过 go help test
可以看到go test的使用说明。
$ go help test
usage: go test [build/test flags] [packages] [build/test flags & test binary flags]
'Go test' automates testing the packages named by the import paths.
It prints a summary of the test results in the format:
ok archive/tar 0.011s
FAIL archive/zip 0.022s
ok compress/gzip 0.033s
...
In addition to the build flags, the flags handled by 'go test' itself are:
-args
Pass the remainder of the command line (everything after -args)
to the test binary, uninterpreted and unchanged.
Because this flag consumes the remainder of the command line,
the package list (if present) must appear before this flag.
-c
Compile the test binary to pkg.test but do not run it
(where pkg is the last element of the package's import path).
The file name can be changed with the -o flag.
-exec xprog
Run the test binary using xprog. The behavior is the same as
in 'go run'. See 'go help run' for details.
-i
Install packages that are dependencies of the test.
Do not run the test.
The -i flag is deprecated. Compiled packages are cached automatically.
-json
Convert test output to JSON suitable for automated processing.
See 'go doc test2json' for the encoding details.
-o file
Compile the test binary to the named file.
The test still runs (unless -c or -i is specified).
格式形如: go test [build/test flags] [packages] [build/test flags & test binary flags]
参数解读:
-
-c
:生成用于运行测试的可执行文件,但不执行它。这个可执行文件会被命名为 pkg.test,其中的 pkg 即为被测试代码包的导入路径的最后一个元素的名称。
-
-i
:安装/重新安装运行测试所需的依赖包,但不编译和运行测试代码。 -
-o
:指定用于运行测试的可执行文件的名称,追加该标记不会影响测试代码的运行,除非同时追加了标记-c 或 -i。
上述这几个标记可以搭配使用,搭配使用的目的可以是让 go test 命令既安装依赖包又编译测试代码,但不运行
测试。也就是说,让命令程序跑一遍运行测试之前的所有流程。这可以测试一下测试过程。注意,在加入 -c 标记
后,命令程序会把用于运行测试的可执行文件存放到当前目录下。
关于 build/test flags
,调用 go help build
,这些是编译运行过程中需要使用到的参数,一般设置为空。
关于 packages
,调用 go help packages
,这些是关于包的管理,一般设置为空。
关于 test binary flags
,调用 go help testflag
,这些是 go test 过程中经常使用到的参数:
-
-test.v
:是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。
-
-test.run pattern
:只跑哪些单元测试用例。 -
-test.bench patten
:只跑那些性能测试用例。 -
-test.benchmem
:是否在性能测试的时候输出内存情况。 -
-test.benchtime t
:性能测试运行的时间,默认是1s。 -
-test.cpuprofile cpu.out
:是否输出cpu性能分析文件。 -
-test.memprofile mem.out
:是否输出内存性能分析文件。 -
-test.blockprofile block.out
:是否输出内部goroutine阻塞的性能分析文件。 -
-test.memprofilerate n
:内存性能分析的时候有一个分配了多少的时候才打点记录的问题。这个参数就是设置打点的内存分配间隔,也就是profile中一个sample代表的内存大小。默认是设置为512 * 1024的。如
果你将它设置为1,则每分配一个内存块就会在profile中有个打点,那么生成的profile的sample就会非常多。
如果你设置为0,那就是不做打点了。你可以通过设置memprofilerate=1和GOGC=off来关闭内存回收,并且
对每个内存块的分配进行观察。
-
-test.blockprofilerate n
:基本同上,控制的是goroutine阻塞时候打点的纳秒数。默认不设置就相当于
-test.blockprofilerate=1
,每一纳秒都打点记录一下。 -
-test.parallel n
:性能测试的程序并行cpu数,默认等于GOMAXPROCS。 -
-test.timeout t
:如果测试用例运行时间超过t,则抛出panic。 -
-test.cpu 1,2,4
:go test -cpu=1,2,4 将会执行 3 次,其中 GOMAXPROCS 值分别为 1,2,和 4。 -
-test.short
:将那些运行时间较长的测试用例运行时间缩短。 -
-test.outputdir
:输出目录。 -
-test.coverprofil
:测试覆盖率参数,指定输出文件。
所有的 test 参数:
-test.bench regexp
run only benchmarks matching regexp
-test.benchmem
print memory allocations for benchmarks
-test.benchtime d
run each benchmark for duration d (default 1s)
-test.blockprofile file
write a goroutine blocking profile to file
-test.blockprofilerate rate
set blocking profile rate (see runtime.SetBlockProfileRate) (default 1)
-test.count n
run tests and benchmarks n times (default 1)
-test.coverprofile file
write a coverage profile to file
-test.cpu list
comma-separated list of cpu counts to run each test with
-test.cpuprofile file
write a cpu profile to file
-test.failfast
do not start new tests after the first test failure
-test.fuzz regexp
run the fuzz test matching regexp
-test.fuzzcachedir string
directory where interesting fuzzing inputs are stored (for use only by cmd/go)
-test.fuzzminimizetime value
time to spend minimizing a value after finding a failing input (default 1m0s)
-test.fuzztime value
time to spend fuzzing; default is to run indefinitely
-test.fuzzworker
coordinate with the parent process to fuzz random values (for use only by cmd/go)
-test.list regexp
list tests, examples, and benchmarks matching regexp then exit
-test.memprofile file
write an allocation profile to file
-test.memprofilerate rate
set memory allocation profiling rate (see runtime.MemProfileRate)
-test.mutexprofile string
write a mutex contention profile to the named file after execution
-test.mutexprofilefraction int
if >= 0, calls runtime.SetMutexProfileFraction() (default 1)
-test.outputdir dir
write profiles to dir
-test.paniconexit0
panic on call to os.Exit(0)
-test.parallel n
run at most n tests in parallel (default 20)
-test.run regexp
run only tests and examples matching regexp
-test.short
run smaller test suite to save time
-test.shuffle string
randomize the execution order of tests and benchmarks (default "off")
-test.testlogfile file
write test action log to file (for use only by cmd/go)
-test.timeout d
panic test binary after duration d (default 0, timeout disabled)
-test.trace file
write an execution trace to file
-test.v
verbose: print additional output
# 带参数的例子
# 其它的参数视情况决定是否添加
$ go test -v -parallel 2
1.1 Go test 注意事项
GO 语言里面的单元测试,是使用标准库 testing
。
注意事项如下:
- 用来测试的代码必须以
_test.go
结尾。 - 单元测试的函数名必须以
Test开头
,并且只有一个参数,类型是*testing.T
。 - 单元测试函数名
Test
后必须紧跟着大写,比如:TestAdd
。 - 基准测试或压力测试必须以
Benchmark
开头,并且只有参数*testing.B
。
例如:
// Tests
func TestXxx(*testing.T)
package go_test
import (
"testing"
"math"
)
func TestAbs(t *testing.T) {
got := math.Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
}
$ go test -v 001_test.go
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok command-line-arguments 0.258s
// Benchmarks
func BenchmarkXxx(*testing.B)
package go_test
import (
"math/rand"
"testing"
)
func BenchmarkRandInt(b *testing.B) {
for i := 0; i < b.N; i++ {
rand.Int()
}
}
$ go test -bench BenchmarkRandInt -cpu=1 -count=5 002_test.go
goos: windows
goarch: amd64
cpu: 12th Gen Intel(R) Core(TM) i7-12700
BenchmarkRandInt 100000000 11.43 ns/op
BenchmarkRandInt 100000000 11.50 ns/op
BenchmarkRandInt 100000000 11.50 ns/op
BenchmarkRandInt 100000000 11.46 ns/op
BenchmarkRandInt 100000000 11.49 ns/op
PASS
ok command-line-arguments 6.038s
基准函数必须运行目标代码 b.N 次,在基准测试执行期间,b.N 被调整,直到基准测试函数持续足够长的时间以
可靠地定时。
如果基准测试在运行前需要一些昂贵的设置,则可以重置计时器:b.ResetTimer()
。
1.2 Go test命令介绍
go test packageName
:执行这个包下面的所有测试用例。go test .
:执行当前目录下的所有测试用例。go test 测试源文件
:执行这个测试源文件里的所有测试用例。go test -run 选项
:执行指定的测试用例。go test -bench .
:执行当前路径下的所有压力测试。go test -bench BenchmarkAdd
:对BenchmarkAdd这个函数执行压力测试。
1.3 利用单元测试来测试斐波那契数列的递归和非递归版本
斐波那契数列的计算:
package fb
// 斐波那契数列的递归版本
// 斐波那契数列:后一个数等于前面两个数的和
func FbRecursion(num int) int {
if num <= 2 {
return 1
}
return FbRecursion(num-1) + FbRecursion(num-2)
}
// 斐波那契数列的非递归版本
// 斐波那契数列:后一个数等于前面两个数的和
func FbNoRecursion(num int) int {
l := make([]int, num+1)
for i := 1; i <= num; i++ {
if i <= 2 {
l[i] = 1
} else {
l[i] = l[i-1] + l[i-2]
}
}
return l[num]
}
package go_test
import (
"proj/fb"
"testing"
)
type FbTestCase struct {
num int
expect int
}
var fbTestCases = map[string]FbTestCase{
"1": {num: 1, expect: 1},
"2": {num: 2, expect: 1},
"3": {num: 3, expect: 2},
"4": {num: 4, expect: 3},
"5": {num: 5, expect: 5},
"9": {num: 9, expect: 34},
}
// 基准测试
// 递归测试
func TestFbRecursion(t *testing.T) {
for name, tc := range fbTestCases {
// 使用t.Run执行子测试
t.Run(name, func(tt *testing.T) {
output := fb.FbRecursion(tc.num)
if output != tc.expect {
tt.Errorf("expect: %d, but output: %d", tc.expect, output)
}
})
}
}
// 非递归测试
func TestFbNoRecursion(t *testing.T) {
for name, tc := range fbTestCases {
// 使用t.Run执行子测试
t.Run(name, func(tt *testing.T) {
output := fb.FbNoRecursion(tc.num)
if output != tc.expect {
tt.Errorf("expect: %d, but output: %d", tc.expect, output)
}
})
}
}
使用如下命令进行测试,测试结果:
$ go test -run .*Fb.* -v
=== RUN TestFbRecursion
=== RUN TestFbRecursion/9
=== RUN TestFbRecursion/1
=== RUN TestFbRecursion/2
=== RUN TestFbRecursion/3
=== RUN TestFbRecursion/4
=== RUN TestFbRecursion/5
--- PASS: TestFbRecursion (0.00s)
--- PASS: TestFbRecursion/9 (0.00s)
--- PASS: TestFbRecursion/1 (0.00s)
--- PASS: TestFbRecursion/2 (0.00s)
--- PASS: TestFbRecursion/3 (0.00s)
--- PASS: TestFbRecursion/4 (0.00s)
--- PASS: TestFbRecursion/5 (0.00s)
=== RUN TestFbNoRecursion
=== RUN TestFbNoRecursion/2
=== RUN TestFbNoRecursion/3
=== RUN TestFbNoRecursion/4
=== RUN TestFbNoRecursion/5
=== RUN TestFbNoRecursion/9
=== RUN TestFbNoRecursion/1
--- PASS: TestFbNoRecursion (0.00s)
--- PASS: TestFbNoRecursion/2 (0.00s)
--- PASS: TestFbNoRecursion/3 (0.00s)
--- PASS: TestFbNoRecursion/4 (0.00s)
--- PASS: TestFbNoRecursion/5 (0.00s)
--- PASS: TestFbNoRecursion/9 (0.00s)
--- PASS: TestFbNoRecursion/1 (0.00s)
PASS
ok proj 0.256s
生成测试的二进制文件:
$ go test -v -run="TestFbRecursion" -c
# 会生成proj.test.exe文件
1.4 测试支持的函数
使用 go doc testing.T
查询文档:
$ go doc testing.T
package testing // import "testing"
type T struct {
// Has unexported fields.
}
T is a type passed to Test functions to manage test state and support
formatted test logs.
A test ends when its Test function returns or calls any of the methods
FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods, as well as
the Parallel method, must be called only from the goroutine running the Test
function.
The other reporting methods, such as the variations of Log and Error, may be
called simultaneously from multiple goroutines.
func (c *T) Cleanup(f func())
func (t *T) Deadline() (deadline time.Time, ok bool)
func (c *T) Error(args ...any)
func (c *T) Errorf(format string, args ...any)
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...any)
func (c *T) Fatalf(format string, args ...any)
func (c *T) Helper()
func (c *T) Log(args ...any)
func (c *T) Logf(format string, args ...any)
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (t *T) Setenv(key, value string)
func (c *T) Skip(args ...any)
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...any)
func (c *T) Skipped() bool
func (c *T) TempDir() string
go doc testing testing.T.Cleanup
查询Cleanup的方法描述:
$ go doc testing testing.T.Cleanup
doc: too many periods in symbol specification
Usage of [go] doc:
go doc
go doc <pkg>
go doc <sym>[.<methodOrField>]
go doc [<pkg>.]<sym>[.<methodOrField>]
go doc [<pkg>.][<sym>.]<methodOrField>
go doc <pkg> <sym>[.<methodOrField>]
For more information run
go help doc
Flags:
-all
show all documentation for package
-c symbol matching honors case (paths not affected)
-cmd
show symbols with package docs even if package is a command
-short
one-line representation for each symbol
-src
show source code for symbol
-u show unexported symbols as well as exported
exit status 2
1.5 跳过某些测试用例
通过调用 *T
或 *B
的 Skip 方法,可以在运行时跳过测试或基准测试:
package go_test
import (
"math"
"testing"
)
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}else{
got := math.Abs(-10)
if got != 10 {
t.Errorf("Abs(-1) = %f; want 10", got)
}
}
}
当执行 go test -short
时就不会执行上面的 TestTimeConsuming 测试用例。
$ go test 004_test.go -v -short
=== RUN TestTimeConsuming
004_test.go:10: skipping test in short mode.
--- SKIP: TestTimeConsuming (0.00s)
PASS
ok command-line-arguments 0.264s
$ go test 004_test.go -v
=== RUN TestTimeConsuming
--- PASS: TestTimeConsuming (0.00s)
PASS
ok command-line-arguments 0.238s
1.6 子测试
Go1.7+中新增了子测试,支持在测试函数中使用 t.Run 执行一组测试用例,这样就不需要为不同的测试数据定义
多个测试函数了。
func TestFoo(t *testing.T) {
// <setup code>
t.Run("A=1", func(t *testing.T) { ... })
t.Run("A=2", func(t *testing.T) { ... })
t.Run("B=1", func(t *testing.T) { ... })
// <tear-down code>
}
go test -run '' # Run all tests.
go test -run Foo # Run top-level tests matching "Foo", such as "TestFooBar".
go test -run Foo/A= # For top-level tests matching "Foo", run subtests matching "A=".
go test -run /A=1 # For all top-level tests, run subtests matching "A=1".
go test -fuzz FuzzFoo # Fuzz the target matching "FuzzFoo"
例子:
package go_test
import "testing"
import "fmt"
func myAdd(a int, b int) int {
if a+b > 10 {
return 10
}
return a + b
}
func mySub(one int, two int) int {
if one-two < 0 {
return 1
}
return one - two
}
func TestMyAdd(t *testing.T) {
num := myAdd(4, 9)
fmt.Println(num)
num = myAdd(4, 2)
fmt.Println(num)
}
func TestMySub(t *testing.T) {
t.Run("one", func(t *testing.T) {
if mySub(2, 3) != 1 {
t.Fatal("cal error")
}
})
t.Run("two", func(t *testing.T) {
if mySub(3, 1) != 2 {
t.Fatal(" error ")
}
})
}
单独调用子测试函数,执行:
$ go test -run TestMySub/one -v
=== RUN TestMySub
=== RUN TestMySub/one
--- PASS: TestMySub (0.00s)
--- PASS: TestMySub/one (0.00s)
PASS
ok proj 0.217s
$ go test -run TestMySub/two -v
=== RUN TestMySub
=== RUN TestMySub/two
--- PASS: TestMySub (0.00s)
--- PASS: TestMySub/two (0.00s)
PASS
ok proj 0.261s
1.7 Fuzzing
go test 和测试包支持 fuzzing,这是一种测试技术,通过随机生成的输入调用函数来发现单元测试没有预料到的错
误。
func FuzzXxx(*testing.F)
package go_test
import (
"bytes"
"encoding/hex"
"testing"
)
func FuzzHex(f *testing.F) {
for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, in []byte) {
enc := hex.EncodeToString(in)
out, err := hex.DecodeString(enc)
if err != nil {
t.Fatalf("%v: decode: %v", in, err)
}
if !bytes.Equal(in, out) {
t.Fatalf("%v: not equal after round trip: %v", in, out)
}
})
}
$ go test -v -fuzz FuzzHex 006_test.go
=== FUZZ FuzzHex
fuzz: elapsed: 0s, gathering baseline coverage: 0/28 completed
fuzz: elapsed: 0s, gathering baseline coverage: 28/28 completed, now fuzzing with 20 workers
fuzz: elapsed: 3s, execs: 2269065 (754894/sec), new interesting: 0 (total: 28)
fuzz: elapsed: 6s, execs: 4247710 (660281/sec), new interesting: 0 (total: 28)
fuzz: elapsed: 9s, execs: 6091725 (614495/sec), new interesting: 0 (total: 28)
fuzz: elapsed: 12s, execs: 7923134 (609359/sec), new interesting: 0 (total: 28)
fuzz: elapsed: 15s, execs: 9752940 (610984/sec), new interesting: 0 (total: 28)
fuzz: elapsed: 18s, execs: 11603037 (612920/sec), new interesting: 0 (total: 28)
fuzz: elapsed: 21s, execs: 13436307 (615324/sec), new interesting: 0 (total: 28)
fuzz: elapsed: 24s, execs: 15295542 (615446/sec), new interesting: 0 (total: 28)
fuzz: elapsed: 27s, execs: 16936635 (587500/sec), new interesting: 0 (total: 28)
--- PASS: FuzzHex (26.82s)
PASS
ok command-line-arguments 27.178s
可以设置 -fuzz 标志,以便模糊目标,跳过所有其他测试的执行。
1.8 表格驱动测试
测试讲究 case 覆盖,按上面的方式,要覆盖更多 case 时,显然通过修改代码的方式很笨拙。这时可以采用
Table-Driven 的方式写测试,标准库中有很多测试是使用这种方式写的。
表格驱动测试的步骤通常是定义一个测试用例表格,然后遍历表格,并使用t.Run对每个条目执行必要的测试。
package go_test
import (
"proj/fb"
"testing"
)
func TestFib(t *testing.T) {
var fibTests = []struct {
in int // input
expected int // expected result
}{
{1, 1},
{2, 1},
{3, 2},
{4, 3},
{5, 5},
{6, 8},
{7, 13},
}
for _, tt := range fibTests {
actual := fb.FbRecursion(tt.in)
if actual != tt.expected {
t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
}
}
}
$ go test 007_test.go -v
=== RUN TestFib
--- PASS: TestFib (0.00s)
PASS
ok command-line-arguments 0.248s
1.9 并行测试
表格驱动测试中通常会定义比较多的测试用例,而Go语言又天生支持并发,所以很容易发挥自身并发优势将表格
驱动测试并行化。 想要在单元测试过程中使用并行测试,可以像下面的代码示例中那样通过添加t.Parallel()来实
现。
package go_test
import (
"reflect"
"testing"
"strings"
)
func TestSplitAll(t *testing.T) {
// 将TLog标记为能够与其他测试并行运行
t.Parallel()
// 定义测试表格
// 为每个测试用例设置了一个名称
tests := []struct {
name string
input string
sep string
want []string
}{
{"base case", "a:b:c", ":", []string{"a", "b", "c"}},
{"wrong sep", "a:b:c", ",", []string{"a:b:c"}},
{"more sep", "abcd", "bc", []string{"a", "d"}},
{"leading sep", "沙河有沙又有河", "沙", []string{"", "河有", "又有河"}},
}
// 遍历测试用例
for _, tt := range tests {
// 注意这里重新声明tt变量(避免多个goroutine中使用了相同的变量)
tt := tt
// 使用t.Run()执行子测试
t.Run(tt.name, func(t *testing.T) {
// 将每个测试用例标记为能够彼此并行运行
t.Parallel()
got := strings.Split(tt.input, tt.sep)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("expected:%#v, got:%#v", tt.want, got)
}
})
}
}
$ go test 008_test.go -v
=== RUN TestSplitAll
=== PAUSE TestSplitAll
=== CONT TestSplitAll
=== RUN TestSplitAll/base_case
=== PAUSE TestSplitAll/base_case
=== RUN TestSplitAll/wrong_sep
=== PAUSE TestSplitAll/wrong_sep
=== RUN TestSplitAll/more_sep
=== PAUSE TestSplitAll/more_sep
=== RUN TestSplitAll/leading_sep
=== PAUSE TestSplitAll/leading_sep
=== CONT TestSplitAll/base_case
=== CONT TestSplitAll/wrong_sep
=== CONT TestSplitAll/leading_sep
=== CONT TestSplitAll/more_sep
--- PASS: TestSplitAll (0.00s)
--- PASS: TestSplitAll/base_case (0.00s)
--- PASS: TestSplitAll/wrong_sep (0.00s)
--- PASS: TestSplitAll/leading_sep (0.00s)
--- PASS: TestSplitAll/more_sep (0.00s)
PASS
ok command-line-arguments 0.237s
1.10 Main
测试或基准测试程序有时需要在执行之前或之后进行额外的设置和拆卸,有时还需要控制哪些代码在主线程上运
行,为了支持这些和其他情况,如果测试文件包含以下函数:
func TestMain(m *testing.M)
那么生成的测试将调用 TestMain,而不是直接运行测试或基准测试。TestMain 在主 goroutine 中运行,可以围绕
对 m.Run 的调用进行任何必要的设置和拆卸。m.Run 将返回一个可能传递给 os.Exit 的退出代码。如果 TestMain
返回,测试包装器将把 m.Run 的结果传递给 os.Exit 本身。
当调用 TestMain 时,flag.Parse 尚未运行。如果 TestMain 依赖于命令行标志,包括测试包的标志,它应该显式
调用 flag.Parse。命令行标志总是在测试或基准函数运行时解析。
TestMain的一个简单实现是:
package go_test
import (
"fmt"
"math"
"os"
"testing"
)
func TestMain(m *testing.M) {
// 测试之前的做一些设置
fmt.Println("write setup code here...")
// 如果 TestMain 使用了 flags,这里应该加上flag.Parse()
// 执行测试
retCode := m.Run()
// 测试之后做一些拆卸工作
fmt.Println("write teardown code here...")
// 退出测试
os.Exit(retCode)
}
func TestAbs(t *testing.T) {
got := math.Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
}
$ go test 009_test.go -v
write setup code here...
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
PASS
write teardown code here...
ok command-line-arguments 0.248s
1.11 Setup与Teardown
有时候可能需要为每个测试集设置Setup与Teardown,也有可能需要为每个子测试设置Setup与Teardown。
package go_test
import (
"reflect"
"strings"
"testing"
)
// 测试集的Setup与Teardown
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("如有需要在此执行:测试之前的setup")
// 返回Teardown
return func(t *testing.T) {
t.Log("如有需要在此执行:测试之后的teardown")
}
}
// 子测试的Setup与Teardown
func setupSubTest(t *testing.T) func(t *testing.T) {
t.Log("如有需要在此执行:子测试之前的setup")
// 返回Teardown
return func(t *testing.T) {
t.Log("如有需要在此执行:子测试之后的teardown")
}
}
func TestSplit(t *testing.T) {
// 定义test结构体
type test struct {
input string
sep string
want []string
}
// 测试用例使用map存储
tests := map[string]test{
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}},
}
// 测试之前执行setup操作
teardownTestCase := setupTestCase(t)
// 测试之后执行testdoen操作
defer teardownTestCase(t)
for name, tc := range tests {
// 使用t.Run()执行子测试
t.Run(name, func(t *testing.T) {
// 子测试之前执行setup操作
teardownSubTest := setupSubTest(t)
// 测试之后执行testdoen操作
defer teardownSubTest(t)
got := strings.Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("expected:%#v, got:%#v", tc.want, got)
}
})
}
}
$ go test 017_test.go -v
=== RUN TestSplit
017_test.go:11: 如有需要在此执行:测试之前的setup
=== RUN TestSplit/simple
017_test.go:20: 如有需要在此执行:子测试之前的setup
017_test.go:23: 如有需要在此执行:子测试之后的teardown
=== RUN TestSplit/wrong_sep
017_test.go:20: 如有需要在此执行:子测试之前的setup
017_test.go:23: 如有需要在此执行:子测试之后的teardown
=== RUN TestSplit/more_sep
017_test.go:20: 如有需要在此执行:子测试之前的setup
017_test.go:23: 如有需要在此执行:子测试之后的teardown
=== RUN TestSplit/leading_sep
017_test.go:20: 如有需要在此执行:子测试之前的setup
017_test.go:23: 如有需要在此执行:子测试之后的teardown
=== CONT TestSplit
017_test.go:14: 如有需要在此执行:测试之后的teardown
--- PASS: TestSplit (0.00s)
--- PASS: TestSplit/simple (0.00s)
--- PASS: TestSplit/wrong_sep (0.00s)
--- PASS: TestSplit/more_sep (0.00s)
--- PASS: TestSplit/leading_sep (0.00s)
PASS
ok command-line-arguments 0.228s
1.12 覆盖率测试
测试覆盖率是指代码被测试套件覆盖的百分比,也就是在测试中至少被运行一次的代码占总代码的比例,在公司内
部一般会要求测试覆盖率达到80%左右。
查看测试覆盖率:
$ go test -cover -run .*Fb.* -coverprofile='c.out' -coverpkg=proj/fb
-
-cover
:代表进行覆盖率测试 -
-coverprofile
:代表将结果存储到c.out -
-coverpkg
:用于指定覆盖率测试的目标包,测试代码所依赖的源文件所在包。这就是说,不是目录下的.go文件,而是直接是包就可以了
$ go test -cover -run .*Fb.* -coverprofile='c.out' -coverpkg=proj/fb
PASS
coverage: 100.0% of statements in proj/fb
ok proj 0.286s
生成的测试结果,可使用 go tool cover -html='c.out' -o coverage.html
来转换成可视化的html:
$ go tool cover -html='c.out' -o coverage.html
查看生成的 html 内容:
图中绿色的部分是已覆盖,红色的部分是未覆盖,咱们的例子已经全部覆盖具体的函数功能。
1.13 多个文件同时进行测试
$ go test -o 001_test.go 002_test.go -v
# 或者
$ go test 001_test.go 002_test.go -v
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok command-line-arguments 0.236s
go test 如果不指定文件,则测试的是当前目录下的所有文件。
1.14 报告方法
遇到一个断言错误的时候,标识这个测试失败:
Fail() : 测试失败,测试继续
FailNow() : 测试失败,测试中断
遇到一个断言错误,只希望跳过这个错误,但是不希望标识测试失败:
SkipNow() : 跳过测试,测试中断
只希望打印信息:
Log : 输出信息
Logf : 输出格式化的信息
希望跳过这个测试,并且打印出信息:
Skip : 相当于 Log + SkipNow
Skipf : 相当于 Logf + SkipNow
希望断言失败的时候,标识测试失败,并打印出必要的信息,但是测试继续:
Error : 相当于 Log + Fail
Errorf : 相当于 Logf + Fail
希望断言失败的时候,标识测试失败,打印出必要的信息,但中断测试:
Fatal : 相当于 Log + FailNow
Fatalf : 相当于 Logf + FailNow
2、示例函数
实例函数的编写可以使用 go help testfunc
查看:
$ go help testfunc
The 'go test' command expects to find test, benchmark, and example functions
in the "*_test.go" files corresponding to the package under test.
A test function is one named TestXxx (where Xxx does not start with a
lower case letter) and should have the signature,
func TestXxx(t *testing.T) { ... }
A benchmark function is one named BenchmarkXxx and should have the signature,
func BenchmarkXxx(b *testing.B) { ... }
A fuzz test is one named FuzzXxx and should have the signature,
func FuzzXxx(f *testing.F) { ... }
An example function is similar to a test function but, instead of using
*testing.T to report success or failure, prints output to os.Stdout.
If the last comment in the function starts with "Output:" then the output
is compared exactly against the comment (see examples below). If the last
comment begins with "Unordered output:" then the output is compared to the
comment, however the order of the lines is ignored. An example with no such
comment is compiled but not executed. An example with no text after
"Output:" is compiled, executed, and expected to produce no output.
Godoc displays the body of ExampleXxx to demonstrate the use
of the function, constant, or variable Xxx. An example of a method M with
receiver type T or *T is named ExampleT_M. There may be multiple examples
for a given function, constant, or variable, distinguished by a trailing _xxx,
where xxx is a suffix not beginning with an upper case letter.
Here is an example of an example:
func ExamplePrintln() {
Println("The output of\nthis example.")
// Output: The output of
// this example.
}
Here is another example where the ordering of the output is ignored:
func ExamplePerm() {
for _, value := range Perm(4) {
fmt.Println(value)
}
// Unordered output: 4
// 2
// 1
// 3
// 0
}
The entire test file is presented as the example when it contains a single
example function, at least one other function, type, variable, or constant
declaration, and no tests, benchmarks, or fuzz tests.
See the documentation of the testing package for more information.
实例:
package go_test
import (
"fmt"
"proj/fb"
)
func Example_fb_norecursion() {
fmt.Println(fb.FbRecursion(1))
fmt.Println(fb.FbNoRecursion(3))
// Output:
// 1
// 2
}
示例函数也可以当作测试函数进行运行,运行结果需要与Output一致:
$ go test 010_test.go -v
=== RUN Example_fb_norecursion
--- PASS: Example_fb_norecursion (0.00s)
PASS
ok command-line-arguments 0.238s
如果修改为:
package go_test
import (
"fmt"
"proj/fb"
)
func Example_fb_norecursion() {
fmt.Println(fb.FbRecursion(1))
fmt.Println(fb.FbNoRecursion(3))
// Output:
// 1
// 4
}
$ go test 010_test.go -v
=== RUN Example_fb_norecursion
--- FAIL: Example_fb_norecursion (0.00s)
got:
1
2
want:
1
4
FAIL
FAIL command-line-arguments 0.245s
FAIL
示例函数只要包含了// Output:
也是可以通过 go test 运行的可执行测试。
$ go test -run Example
PASS
ok proj 0.272s
// 示例
package go_test
import "fmt"
func ExampleHello() {
fmt.Println("hello")
// Output: hello
}
func ExampleSalutations() {
fmt.Println("hello, and")
fmt.Println("goodbye")
// Output:
// hello, and
// goodbye
}
$ go test 011_test.go -v
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN ExampleSalutations
--- PASS: ExampleSalutations (0.00s)
PASS
ok command-line-arguments 0.223s
前缀 Unordered output:
和 Output:
相似,但是它不按照顺序匹配。
package go_test
import "fmt"
func ExamplePerm() {
list := []int{0,1,2,3,4}
for _, value := range list {
fmt.Println(value)
}
// Unordered output: 4
// 2
// 1
// 3
// 0
}
$ go test 012_test.go -v
=== RUN ExamplePerm
--- PASS: ExamplePerm (0.00s)
PASS
ok command-line-arguments 0.220s
没有 Outout 注释的示例函数被编译但不执行。
一个包/类型/函数/方法的多个示例函数可以通过在名称后面附加一个不同的后缀来提供,后缀必须以小写字母开
头。
// package
func Example() { ... }
// function
func ExampleF() { ... }
// type
func ExampleT() { ... }
// method
func ExampleT_M() { ... }
func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }
3、性能测试
性能测试就是在一定的工作负载之下检测程序性能的一种方法。
func BenchmarkName(b *testing.B){
// ...
}
基准测试以 Benchmark 为前缀,需要一个 testing.B 类型的参数 b,基准测试必须要执行 b.N 次,这样的测试才
有对照性,b.N 的值是系统根据实际情况去调整的,从而保证测试的稳定性。
3.1 支持的函数
使用 go doc testing.B
查询文档:
$ go doc testing.B
package testing // import "testing"
type B struct {
N int
// Has unexported fields.
}
B is a type passed to Benchmark functions to manage benchmark timing and to
specify the number of iterations to run.
A benchmark ends when its Benchmark function returns or calls any of the
methods FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods must
be called only from the goroutine running the Benchmark function. The other
reporting methods, such as the variations of Log and Error, may be called
simultaneously from multiple goroutines.
Like in tests, benchmark logs are accumulated during execution and dumped to
standard output when done. Unlike in tests, benchmark logs are always
printed, so as not to hide output whose existence may be affecting benchmark
results.
func (c *B) Cleanup(f func())
func (c *B) Error(args ...any)
func (c *B) Errorf(format string, args ...any)
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...any)
func (c *B) Fatalf(format string, args ...any)
func (c *B) Helper()
func (c *B) Log(args ...any)
func (c *B) Logf(format string, args ...any)
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ReportMetric(n float64, unit string)
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) Setenv(key, value string)
func (c *B) Skip(args ...any)
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...any)
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()
func (c *B) TempDir() string
3.2 简单例子
package go_test
import (
"fmt"
"testing"
)
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Println("hello world!")
}
}
# 通过go test命令,加上-bench标志来执行
$ go test -bench BenchmarkHello
......
hello world!
hello world!
hello world!
BenchmarkHello-20 66876 16921 ns/op
PASS
ok proj 1.586s
3.3 性能比较函数
通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。
默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按
1,2,5,10,20,50,…增加,并且函数再次运行。
可以使用 -benchtime 标志增加最小基准时间,以产生更准确的结果。
package go_test
import (
"proj/fb"
"testing"
)
func BenchmarkFib40(b *testing.B) {
for i := 0; i < b.N; i++ {
fb.FbRecursion(40)
}
}
func BenchmarkFib40No(b *testing.B) {
for i := 0; i < b.N; i++ {
fb.FbNoRecursion(40)
}
}
$ go test -bench=Fib40 -benchtime=20s
goos: windows
goarch: amd64
pkg: proj
cpu: 12th Gen Intel(R) Core(TM) i7-12700
BenchmarkFib40-20 100 257579890 ns/op
BenchmarkFib40No-20 154547244 163.6 ns/op
PASS
ok proj 67.233s
3.4 计时方法
有三个方法用于计时:
StartTimer:开始对测试进行计时。该方法会在基准测试开始时自动被调用,也可以在调用 StopTimer 之后恢复
计时;
StopTimer:停止对测试进行计时。当需要执行一些复杂的初始化操作,并且不想对这些操作进行测量时,就可以
使用这个方法来暂时地停止计时;
ResetTimer:对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法
不会产生任何效果。
package go_test
import (
"fmt"
"testing"
"time"
)
func BenchmarkPrint(b *testing.B) {
// 假设需要做一些耗时的无关操作
time.Sleep(5 * time.Second)
// 重置计时器
b.ResetTimer()
for i := 0; i < b.N; i++ {
fmt.Println("hello world!")
}
}
$ go test -bench=Print -v
......
hello world!
hello world!
hello world!
hello world!
BenchmarkPrintParallel-20 62869 18777 ns/op
PASS
ok proj 22.963s
3.5 并行测试
func (b *B) RunParallel(body func(*PB))
会以并行的方式执行给定的基准测试。
通过 RunParallel 方法能够并行地执行给定的基准测试,RunParallel 通常会与 -cpu 标志一同使用。
b.SetParallelism() 可以设置使用的CPU数。
body 函数将在每个 goroutine 中执行,这个函数需要设置所有 goroutine 本地的状态,并迭代直到 pb.Next 返回
false 值为止。因为 StartTimer、StopTime 和 ResetTimer 这三个方法都带有全局作用,所以 body 函数不应该调
用这些方法; 除此之外,body 函数也不应该调用 Run 方法。
package go_test
import (
"fmt"
"testing"
)
func BenchmarkPrintParallel(b *testing.B) {
// 设置使用的CPU数
// b.SetParallelism(1)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
fmt.Println("hello world!")
}
})
}
$ go test -bench=PrintParallel -v
......
hello world!
hello world!
hello world!
hello world!
BenchmarkPrintParallel-20 61087 18990 ns/op
PASS
ok proj 1.646s
3.6 子测试
package go_test
import (
"fmt"
"testing"
)
type identifier interface {
idInline() int32
idNoInline() int32
}
type id32 struct{ id int32 }
func (id *id32) idInline() int32 { return id.id }
func (id *id32) idNoInline() int32 { return id.id }
var escapeMePlease *id32
func escapeToHeap(id *id32) identifier {
escapeMePlease = id
return escapeMePlease
}
func BenchmarkMethodCall_direct(b *testing.B) {
var myID int32
b.Run("single/noinline", func(b *testing.B) {
m := escapeToHeap(&id32{id: 6754}).(*id32)
b.ResetTimer()
for i := 0; i < b.N; i++ {
myID = m.idNoInline()
fmt.Print(myID)
}
})
b.Run("single/inline", func(b *testing.B) {
m := escapeToHeap(&id32{id: 6754}).(*id32)
b.ResetTimer()
for i := 0; i < b.N; i++ {
myID = m.idInline()
fmt.Print(myID)
}
})
}
func BenchmarkMethodCall_interface(b *testing.B) {
var myID int32
b.Run("single/noinline", func(b *testing.B) {
m := escapeToHeap(&id32{id: 6754})
b.ResetTimer()
for i := 0; i < b.N; i++ {
myID = m.idNoInline()
fmt.Print(myID)
}
})
b.Run("single/inline", func(b *testing.B) {
m := escapeToHeap(&id32{id: 6754})
b.ResetTimer()
for i := 0; i < b.N; i++ {
myID = m.idInline()
fmt.Print(myID)
}
})
}
测试命令:
$ go test -bench BenchmarkMethodCall_direct/single/inline -cpu=1 -count=5 018_test.go
...... 20631 ns/op
PASS
ok command-line-arguments 14.152s
$ go test -bench BenchmarkMethodCall_direct/single/noinline -cpu=1 -count=5 018_test.go
...... 20869 ns/op
PASS
ok command-line-arguments 7.133s
$ go test -bench BenchmarkMethodCall_interface/single/inline -cpu=1 -count=5 018_test.go
...... 20198 ns/op
PASS
ok command-line-arguments 13.932s
$ go test -bench BenchmarkMethodCall_interface/single/noinline -cpu=1 -count=5 018_test.go
...... 21178 ns/op
PASS
ok command-line-arguments 7.070s
$ go test -bench . -cpu=1 -count=5 018_test.go
...... 18983 ns/op
PASS
ok command-line-arguments 28.031s
ns/op
的意思是:ns纳秒/op操作。
4、参考
官方文档:https://pkg.go.dev/testing