Go语言单元测试

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.011sFAIL archive/zip   0.022sok   compress/gzip 0.033s...
In addition to the build flags, the flags handled by 'go test' itself are:-argsPass 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.-cCompile 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 xprogRun the test binary using xprog. The behavior is the same asin 'go run'. See 'go help run' for details.-iInstall packages that are dependencies of the test.Do not run the test.The -i flag is deprecated. Compiled packages are cached automatically.-jsonConvert test output to JSON suitable for automated processing.See 'go doc test2json' for the encoding details.-o fileCompile 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 regexprun only benchmarks matching regexp-test.benchmemprint memory allocations for benchmarks-test.benchtime drun each benchmark for duration d (default 1s)-test.blockprofile filewrite a goroutine blocking profile to file-test.blockprofilerate rateset blocking profile rate (see runtime.SetBlockProfileRate) (default 1)-test.count nrun tests and benchmarks n times (default 1)-test.coverprofile filewrite a coverage profile to file-test.cpu listcomma-separated list of cpu counts to run each test with-test.cpuprofile filewrite a cpu profile to file-test.failfastdo not start new tests after the first test failure-test.fuzz regexprun the fuzz test matching regexp-test.fuzzcachedir stringdirectory where interesting fuzzing inputs are stored (for use only by cmd/go)-test.fuzzminimizetime valuetime to spend minimizing a value after finding a failing input (default 1m0s)-test.fuzztime valuetime to spend fuzzing; default is to run indefinitely-test.fuzzworkercoordinate with the parent process to fuzz random values (for use only by cmd/go)-test.list regexplist tests, examples, and benchmarks matching regexp then exit-test.memprofile filewrite an allocation profile to file-test.memprofilerate rateset memory allocation profiling rate (see runtime.MemProfileRate)-test.mutexprofile stringwrite a mutex contention profile to the named file after execution-test.mutexprofilefraction intif >= 0, calls runtime.SetMutexProfileFraction() (default 1)-test.outputdir dirwrite profiles to dir-test.paniconexit0panic on call to os.Exit(0)-test.parallel nrun at most n tests in parallel (default 20)-test.run regexprun only tests and examples matching regexp-test.shortrun smaller test suite to save time-test.shuffle stringrandomize the execution order of tests and benchmarks (default "off")-test.testlogfile filewrite test action log to file (for use only by cmd/go)-test.timeout dpanic test binary after duration d (default 0, timeout disabled)-test.trace filewrite an execution trace to file-test.vverbose: 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_testimport ("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_testimport ("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_testimport ("proj/fb""testing"
)type FbTestCase struct {num    intexpect 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 supportformatted test logs.A test ends when its Test function returns or calls any of the methodsFailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods, as well asthe Parallel method, must be called only from the goroutine running the Testfunction.The other reporting methods, such as the variations of Log and Error, may becalled 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 docgo doc <pkg>go doc <sym>[.<methodOrField>]go doc [<pkg>.]<sym>[.<methodOrField>]go doc [<pkg>.][<sym>.]<methodOrField>go doc <pkg> <sym>[.<methodOrField>]
For more information rungo help docFlags:-allshow all documentation for package-c    symbol matching honors case (paths not affected)-cmdshow symbols with package docs even if package is a command-shortone-line representation for each symbol-srcshow source code for symbol-u    show unexported symbols as well as exported
exit status 2

1.5 跳过某些测试用例

通过调用 *T*B 的 Skip 方法,可以在运行时跳过测试或基准测试:

package go_testimport ("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   TestTimeConsuming004_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_testimport "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_testimport ("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_testimport ("proj/fb""testing"
)func TestFib(t *testing.T) {var fibTests = []struct {in       int // inputexpected 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_testimport ("reflect""testing""strings"
)func TestSplitAll(t *testing.T) {// 将TLog标记为能够与其他测试并行运行t.Parallel()// 定义测试表格// 为每个测试用例设置了一个名称tests := []struct {name  stringinput stringsep   stringwant  []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_testimport ("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_testimport ("reflect""strings""testing"
)// 测试集的Setup与Teardown
func setupTestCase(t *testing.T) func(t *testing.T) {t.Log("如有需要在此执行:测试之前的setup")// 返回Teardownreturn func(t *testing.T) {t.Log("如有需要在此执行:测试之后的teardown")}
}// 子测试的Setup与Teardown
func setupSubTest(t *testing.T) func(t *testing.T) {t.Log("如有需要在此执行:子测试之前的setup")// 返回Teardownreturn func(t *testing.T) {t.Log("如有需要在此执行:子测试之后的teardown")}
}func TestSplit(t *testing.T) {// 定义test结构体type test struct {input stringsep   stringwant  []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   TestSplit017_test.go:11: 如有需要在此执行:测试之前的setup
=== RUN   TestSplit/simple017_test.go:20: 如有需要在此执行:子测试之前的setup017_test.go:23: 如有需要在此执行:子测试之后的teardown
=== RUN   TestSplit/wrong_sep017_test.go:20: 如有需要在此执行:子测试之前的setup017_test.go:23: 如有需要在此执行:子测试之后的teardown
=== RUN   TestSplit/more_sep017_test.go:20: 如有需要在此执行:子测试之前的setup017_test.go:23: 如有需要在此执行:子测试之后的teardown
=== RUN   TestSplit/leading_sep017_test.go:20: 如有需要在此执行:子测试之前的setup017_test.go:23: 如有需要在此执行:子测试之后的teardown
=== CONT  TestSplit017_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_testimport ("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_testimport ("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_testimport "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_testimport "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 tospecify the number of iterations to run.A benchmark ends when its Benchmark function returns or calls any of themethods FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods mustbe called only from the goroutine running the Benchmark function. The otherreporting methods, such as the variations of Log and Error, may be calledsimultaneously from multiple goroutines.Like in tests, benchmark logs are accumulated during execution and dumped tostandard output when done. Unlike in tests, benchmark logs are alwaysprinted, so as not to hide output whose existence may be affecting benchmarkresults.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_testimport ("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_testimport ("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_testimport ("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_testimport ("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_testimport ("fmt""testing"
)type identifier interface {idInline() int32idNoInline() int32
}type id32 struct{ id int32 }func (id *id32) idInline() int32 { return id.id }func (id *id32) idNoInline() int32 { return id.id }var escapeMePlease *id32func escapeToHeap(id *id32) identifier {escapeMePlease = idreturn escapeMePlease
}func BenchmarkMethodCall_direct(b *testing.B) {var myID int32b.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 int32b.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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/485.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Efficient Video Transformers with Spatial-Temporal Token Selection阅读笔记

摘要 Video Transformers在主要视频识别基准测试中取得了令人印象深刻的结果&#xff0c;但其计算成本很高。 在本文中&#xff0c;我们提出了 STTS&#xff0c;这是一种令牌选择框架&#xff0c;它根据输入视频样本在时间和空间维度上动态选择一些信息丰富的令牌。 具体来说&…

C++ 第四弹动态内存管理

目录 1. C/C程序内存划分 2. C语言中内存管理的方式 3. new /delete 和 new[]/delete[] 4. void* operator new(size_t size) 和 void operator delete(void*) 可以重载的 5. 定义为new表达式 6. 常见的面试题 1. C/C程序内存划分 1. 栈 又叫堆栈 -- 非静态局部变量 / 函数…

Paragon NTFS2023Mac读取、写入外置移动硬盘软件

在我们日常使用电脑时常常会出现NTFS格式分区&#xff0c;那你知道NTFS For Mac是什么&#xff1f;简单的理解就是让你在mac系统下&#xff0c;可以正常读写Windows的Ntfs格式的分区。其中还包括Windows NT 4&#xff0c;2000&#xff0c;XP&#xff0c;2003&#xff0c;Vista&…

金融科技领先者Broadridge选择CloudBees CI来加速软件交付

Broadridge公司是全球金融科技领先者&#xff0c;通过丰富客户参与度、控制风险、优化效率和创造收入增长的解决方案为客户推动业务转型。 借助CloudBees CI&#xff0c;Broadridge为所有使用Jenkins的开发团队提供了集中管理和自助服务的体验。Broadridge能够不断为客户提供新…

Mysql批量插入1000条数据

使用mysql的存储过程 1.现有如下一张表&#xff1a;site_row 2.创建存储过程 CREATE PROCEDURE p01 () BEGIN declare i int; set i1;while i<1000 doINSERT INTO site_row(row_id,row_num) VALUES ( i,i);set ii1; end WHILE;END; 3.执行存储过程 CALL p01(); 4.查看效…

【论文笔记】FASTER SEGMENT ANYTHING:TOWARDS LIGHTWEIGHT SAM FOR MOBILE APPLICATIONS

前脚fast SAM刚发完&#xff0c;后脚mobile SAM就发了 &#xff0c;之前的论文笔记中我一直就认为fast SAM其实应该算是yolo的扩展工作&#xff0c;和原生的SAM架构相去甚远&#xff0c;而且在简介上直接就对&#xff08;gong&#xff09;比&#xff08;ji&#xff09;了FastSA…

多模态学习

什么是多模态学习&#xff1f; 模态 模态是指一些表达或感知事物的方式&#xff0c;每一种信息的来源或者形式&#xff0c;都可以称为一种模态 视频图像文本音频 多模态 多模态即是从多个模态表达或感知事物 多模态学习 从多种模态的数据中学习并且提升自身的算法 多…

springBoot学习——spring+springMVC 集成mybatis 拦截器

目录 引出入门案例&#xff1a;登陆和注册 & 用户信息分页 之 固定的步骤&#xff1a;&#xff08;1&#xff09;建普通项目配置pom.xml文件&#xff08;2&#xff09;写主启动类 application.yml文件【bug】pom.xml文件导了mybatis的包&#xff0c;但是application.yml文…

低代码可视化拖拽编辑器实现方案

一、前言 随着业务不断发展&#xff0c;低代码、无代码平台越来越常见&#xff0c;它降低开发门槛、快速响应业务需求、提升开发效率。零开发经验的业务人员通过可视化拖拽等方式&#xff0c;即可快速搭建各种应用。本文主要是讲解低代码可视化拖拽平台前端展示层面的实现逻辑…

《语文建设》期刊简介及投稿要求

《语文建设》期刊简介及投稿要求 《语文建设》期刊简介&#xff1a; 主管单位:教育部 主办单位&#xff1a;语文出版社有限公司 国际刊号ISSN&#xff1a;1001-8476&#xff1b;国内刊号CN&#xff1a;11-1399/H&#xff1b;邮发代号&#xff1a;2-200 出版周期&#xff1…

ivx低代码开发平台

前言 低代码开发平台&#xff08;Low-Code Development Platform, LCDS&#xff09;为企业和开发者提供了高效的应用开发方式。在2023年&#xff0c;中国的低代码开发平台正在快速发展&#xff0c;以下是其中最受关注的十大平台&#xff1a; iVX&#xff1a;iVX是一款新型的低代…

python机器学习——机器学习相关概念 特征工程

目录 机器学习特征工程1.特征抽取2.特征处理2.1 归一化&#xff1a;传统精确小数据2.2 标准化&#xff1a;大多数情况 3.数据降维3.1特征选择3.2主成分分析PCA 案例&#xff1a;超市订单分析 机器学习 监督学习&#xff1a;输入数据有特征有标签&#xff0c;即有标准答案 分类&…