0


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 testfunction 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 testfunction 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 testfile 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 formore information.

通过

go help test

可以看到go test的使用说明。

$ go helptest
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 filewrite 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 filewrite a coverage profile to file
  -test.cpu list
        comma-separated list of cpu counts to run each test with
  -test.cpuprofile filewrite 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 thenexit
  -test.memprofile filewrite 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 dirwrite 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 filewritetest 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 filewrite 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

例如:

// TestsfuncTestXxx(*testing.T)
package go_test

import("testing""math")funcTestAbs(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
// BenchmarksfuncBenchmarkXxx(*testing.B)
package go_test

import("math/rand""testing")funcBenchmarkRandInt(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        10000000011.43 ns/op
BenchmarkRandInt        10000000011.50 ns/op
BenchmarkRandInt        10000000011.50 ns/op
BenchmarkRandInt        10000000011.46 ns/op
BenchmarkRandInt        10000000011.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

// 斐波那契数列的递归版本// 斐波那契数列:后一个数等于前面两个数的和funcFbRecursion(num int)int{if num <=2{return1}returnFbRecursion(num-1)+FbRecursion(num-2)}// 斐波那契数列的非递归版本// 斐波那契数列:后一个数等于前面两个数的和funcFbNoRecursion(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},}// 基准测试// 递归测试funcTestFbRecursion(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)}})}}// 非递归测试funcTestFbNoRecursion(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")funcTestTimeConsuming(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 testin 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 执行一组测试用例,这样就不需要为不同的测试数据定义

多个测试函数了。

funcTestFoo(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"funcmyAdd(a int, b int)int{if a+b >10{return10}return a + b
}funcmySub(one int, two int)int{if one-two <0{return1}return one - two
}funcTestMyAdd(t *testing.T){
    num :=myAdd(4,9)
    fmt.Println(num)
    num =myAdd(4,2)
    fmt.Println(num)}funcTestMySub(t *testing.T){
    t.Run("one",func(t *testing.T){ifmySub(2,3)!=1{
            t.Fatal("cal error")}})
    t.Run("two",func(t *testing.T){ifmySub(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,这是一种测试技术,通过随机生成的输入调用函数来发现单元测试没有预料到的错

误。

funcFuzzXxx(*testing.F)
package go_test

import("bytes""encoding/hex""testing")funcFuzzHex(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")funcTestFib(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")funcTestSplitAll(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

测试或基准测试程序有时需要在执行之前或之后进行额外的设置和拆卸,有时还需要控制哪些代码在主线程上运

行,为了支持这些和其他情况,如果测试文件包含以下函数:

funcTestMain(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")funcTestMain(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)}funcTestAbs(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与TeardownfuncsetupTestCase(t *testing.T)func(t *testing.T){
    t.Log("如有需要在此执行:测试之前的setup")// 返回Teardownreturnfunc(t *testing.T){
        t.Log("如有需要在此执行:测试之后的teardown")}}// 子测试的Setup与TeardownfuncsetupSubTest(t *testing.T)func(t *testing.T){
    t.Log("如有需要在此执行:子测试之前的setup")// 返回Teardownreturnfunc(t *testing.T){
        t.Log("如有需要在此执行:子测试之后的teardown")}}funcTestSplit(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操作deferteardownTestCase(t)for name, tc :=range tests {// 使用t.Run()执行子测试
        t.Run(name,func(t *testing.T){// 子测试之前执行setup操作
            teardownSubTest :=setupSubTest(t)// 测试之后执行testdoen操作deferteardownSubTest(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 testfunction 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 testfunction 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 testfile 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 formore information.

实例:

package go_test

import("fmt""proj/fb")funcExample_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")funcExample_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:
12
want:
14
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"funcExampleHello(){
    fmt.Println("hello")// Output: hello}funcExampleSalutations(){
    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"funcExamplePerm(){
    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 注释的示例函数被编译但不执行。

一个包/类型/函数/方法的多个示例函数可以通过在名称后面附加一个不同的后缀来提供,后缀必须以小写字母开

头。

// packagefuncExample(){...}// functionfuncExampleF(){...}// typefuncExampleT(){...}// methodfuncExampleT_M(){...}
funcExample_suffix(){...}funcExampleF_suffix(){...}funcExampleT_suffix(){...}funcExampleT_M_suffix(){...}

3、性能测试

性能测试就是在一定的工作负载之下检测程序性能的一种方法。

funcBenchmarkName(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")funcBenchmarkHello(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          6687616921 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")funcBenchmarkFib40(b *testing.B){for i :=0; i < b.N; i++{
        fb.FbRecursion(40)}}funcBenchmarkFib40No(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            100257579890 ns/op
BenchmarkFib40No-20     154547244163.6 ns/op
PASS
ok      proj    67.233s

3.4 计时方法

有三个方法用于计时:

StartTimer:开始对测试进行计时。该方法会在基准测试开始时自动被调用,也可以在调用 StopTimer 之后恢复

计时;

StopTimer:停止对测试进行计时。当需要执行一些复杂的初始化操作,并且不想对这些操作进行测量时,就可以

使用这个方法来暂时地停止计时;

ResetTimer:对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法

不会产生任何效果。

package go_test

import("fmt""testing""time")funcBenchmarkPrint(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          6286918777 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")funcBenchmarkPrintParallel(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          6108718990 ns/op
PASS
ok      proj    1.646s

3.6 子测试

package go_test

import("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 *id32

funcescapeToHeap(id *id32) identifier {
    escapeMePlease = id
    return escapeMePlease
}funcBenchmarkMethodCall_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)}})}funcBenchmarkMethodCall_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
标签: golang

本文转载自: https://blog.csdn.net/qq_30614345/article/details/131445086
版权归原作者 242030 所有, 如有侵权,请联系我们删除。

“Go语言单元测试”的评论:

还没有评论