0


Go Test测试教程

go中测试既有类似有

pytest

中的功能测试,也有

benchMark

的基准测试,以及单元测试(

Unit Tests

UT

).这里从单元测试UT引入本篇的话题,单元测试的重要性不言而喻,尤其在大型项目中跨团队合作时,无法mr合格的代码,很容易影响整个团队的交付进度和质量。或者会说直接debug,但是当你的代码是几千行的时候,这个时候debug似乎也比较累,那单元测试就能覆盖上述情况。

如何写好单元测试呢?

测试用例编写是基础。比如如何编写单个测试函数和单个测试方法,如何做基准测试,如何

Mock

数据等等,对于可测试的代码,高内聚,低耦合是软件工程的基本要求。同样对于测试而言,函数和方法写法不同,测试难度也是不一样的,参数少且类型单一,与其他函数耦合度较低这种函数更容易测试,其他的比如入参较多,且耦合多较高,是输入参数较多这种情况,结合正交法,等价类划分等设计方法,可以大大较少case设计的难度和复杂度,且能提升测试覆盖面。

这里概述一下go中支持的三种测试类型,分别是

单元/功能测试

性能(压力)测试

覆盖率测试

接下来介绍使用Go的标准测试库

testing

进行单元测试

1.单元测试

go语言中推荐测试文件和源代码文件放置在一起,测试文件以

_test.go

结尾,这里可以和pytest进行类比,当前的

package

calculate.go

文件,向测试

calculate.go

中有

Add

Mul

函数,应该新建一个

calculate_test.go

测试文件,所以习惯会将测试文件命名为功能文件_test.go的形式。
在这里插入图片描述

1.1 入门示例

编写go test测试函数时,如下所示,输入test,自动联想
在这里插入图片描述
单元测试文件可以有多个测试用例组成,每个测试用例的名称前缀必须是Test开头

funcTestXxx( t \*testing.T ){//......  }

函数和单测的文件如下:

# calculate.gopackage calculate

funcAdd(a,b int)int{return a+b
}funcMult(a,b int)int{return a *b
}
# calculate_test.gopackage calculate

import"testing"funcTestAdd(t *testing.T){if ans :=Add(2,3); ans !=5{
        t.Errorf("2+3 expected be 5,but %d got",ans)}if ans :=Add(-10,-20); ans !=-30{
        t.Errorf("-10 + -20 expected be -30, but %d got", ans)}}

在这里插入图片描述
在goland终端执行go test

xxx@yyy calculate % ls
calculate.go            calculate_test.gogo.mod
xxx@yyy calculate %go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      calculate       0.137s
go test -v

-v

参数会显示每个用例的测试结果。如果继续编写mul的测试函数.此时calculate_test.go中两个测试函数,如果想仅仅指定其中一个测试用例,比如

TestMul

,可以使用

-run

参数

go test -run TestMul -v
=== RUN   TestMul
--- PASS: TestMul (0.00s)
PASS
ok      calculate       0.531s
go test <module name>/<package name>

用来运行某个

package

内的所有测试用例。

运行当前

package

内的用例:

go test calcuate

go test .

运行子

package

内的用例:

go test calcuate/<package name>

go test ./<package name>

如果想递归测试当前目录下的所有的

package:go test ./...

go test calcuate/...

该参数还支持通配符

*

,和部分正则表达式,如

^

$

1.2 子测试

这里就不得不提到子测试,即所谓的Subtests,该功能是go语言内置的支持的功能,可以在一个测试用例中,根据测试场景使用

t.Run

创建不同的子测试用例:

funcTestMul(t *testing.T){//if ans := Mul(2,3); ans != 6{//    t.Errorf("2*3 expected be 6,but %d got",ans)//}
    t.Run("pos",func(t *testing.T){ifMul(2,3)!=6{
            t.Fatalf("2*3 expected be 6, but %d got",Mul(2,3))}})
    t.Run("neg",func(t *testing.T){ifMul(2,-3)!=-6{
            t.Fatalf("2*-3 expected be 6, but %d got",Mul(2,-3))}})}

除了使用命令之后,可以使用golang中的如下符号
在这里插入图片描述
输出如下所示

/usr/local/Cellar/go/1.19.6/libexec/bin/go tool test2json -t /private/var/folders/cc/wn7xg4yx22d_qp96zw37rrl00000gp/T/___TestMul_in_calculate.test -test.v -test.paniconexit0 -test.run ^\QTestMul\E$
=== RUN   TestMul
=== RUN   TestMul/pos
=== RUN   TestMul/neg
--- PASS: TestMul (0.00s)--- PASS: TestMul/pos (0.00s)--- PASS: TestMul/neg (0.00s)
PASS

Process finished with the exit code 0

📢:之前的例子失败是使用的

t.Error/t.Errorf

,这里使用的是

t.Fatal/Fatalf

,区别在于前者遇到错误不会停止,还会继续执行其他的测试用例,后者遇到❎就会停止。
执行运行其中某个子测试

 calculate %go test -run TestMul/pos -v
=== RUN   TestMul
=== RUN   TestMul/pos
--- PASS: TestMul (0.00s)--- PASS: TestMul/pos (0.00s)
PASS
ok      calculate       1.002s

种类的

Run()

第一个参数是不是类似Pytest中的mark.tag标签,但是上面的写法发现冗余别较多,推荐使用如下的方法

funcTestMul(t *testing.T){//if ans := Mul(2,3); ans != 6{//    t.Errorf("2*3 expected be 6,but %d got",ans)//}//t.Run("pos", func(t *testing.T) {//    if Mul(2, 3) != 6{//        t.Fatalf("2*3 expected be 6, but %d got", Mul(2, 3))//    }//})//t.Run("neg", func(t *testing.T) {//    if Mul(2, -3) != -6{//        t.Fatalf("2*-3 expected be 6, but %d got", Mul(2, -3))//    }//})

    cases :=[]struct{
        Name           string
        A, B, Expected int}{{"pos",2,3,6},{"neg",2,-3,-6},{"zero",0,2,0},}for_, c :=range cases {
        t.Run(c.Name,func(t *testing.T){if ans :=Mul(c.A, c.B); ans != c.Expected {
                t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)}})}}

输出如下所示:

calculate %go test -run TestMul -v
=== RUN   TestMul
=== RUN   TestMul/pos
=== RUN   TestMul/neg
=== RUN   TestMul/zero
--- PASS: TestMul (0.00s)--- PASS: TestMul/pos (0.00s)--- PASS: TestMul/neg (0.00s)--- PASS: TestMul/zero (0.00s)
PASS
ok      calculate       0.343s

上面的用法和pytest中的@pytest.mark.parametrize(‘status’, [‘Pending’, ‘Running’, ‘Success’, ‘Failed’, ‘Timeout’])进行类比,所有用例的测试数据组织在切片cases中,借助于创建子测试,当然目前觉得pytest这样更方便些,go这样写的好处有:

  1. 新增用例非常简单,只需给 cases 新增一条测试数据即可。
  2. 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。
  3. 用例失败时,报错信息的格式比较统一,测试报告易于阅读。

如果数据量较大,或是一些二进制数据,推荐使用相对路径从文件中读取

所以编写测试用例可以抽象总结为以下几点:

  • 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;
  • 测试用例的文件名必须以_test.go结尾;
  • 需要使用 import 导入 testing 包;
  • 测试函数的名称要以Test或Benchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用
  • 文件中可以包含多个测试函数;
  • 单元测试则以(t *testing.T)作为参数,性能测试以(t *testing.B)做为参数;
  • 测试用例文件使用go test命令来执行,源码中不需要 main() 函数作为入口,所有以_test.go结尾的源码文件内以Test开头的函数都会自动执行。

1.3 帮助函数helpers

我们知道在Pytest中公共的东西可以抽象出来放置在contest.py中,并设置使用级别,如session,function等,对于go中的testing,一些重复的逻辑可以抽出来作为公共的帮助函数helpers,这样的好处无需赘言,增加了测试代码的可维护性和可读性,且使得测试用例的逻辑更加紧凑和清晰,接着上面的示例

# calculate_test.gopackage calculate

import"testing"import"testing"type calcCase struct{
    Name           string
    A, B, Expected int}funcCreateMulTestCase(t *testing.T, c *calcCase,){//    t.helpers//    if ans := Mul(c.A, c.B); ans != c.Expected {//        t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)//    }

    t.Helper()
    t.Run(c.Name,func(t *testing.T){if ans :=Mul(c.A, c.B); ans != c.Expected {
            t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)}})}funcTestMul(t *testing.T){//if ans := Mul(2,3); ans != 6{//    t.Errorf("2*3 expected be 6,but %d got",ans)//}//t.Run("pos", func(t *testing.T) {//    if Mul(2, 3) != 6{//        t.Fatalf("2*3 expected be 6, but %d got", Mul(2, 3))//    }//})//t.Run("neg", func(t *testing.T) {//    if Mul(2, -3) != -6{//        t.Fatalf("2*-3 expected be 6, but %d got", Mul(2, -3))//    }//})//cases := []struct {//    Name           string//    A, B, Expected int//}{//    {"pos", 2, 3, 6},//    {"neg", 2, -3, -6},//    {"zero", 0, 2, -1},//}////for _, c := range cases {//    t.Run(c.Name, func(t *testing.T) {//        if ans := Mul(c.A, c.B); ans != c.Expected {//            t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)//        }//    })//}//CreateMulTestCase(t,&calcCase{2,3,6})//CreateMulTestCase(t,&calcCase{2,-3,-6})//CreateMulTestCase(t,&calcCase{0,2,1})CreateMulTestCase(t,&calcCase{"pos",2,3,6})CreateMulTestCase(t,&calcCase{"neg",2,-3,-6})CreateMulTestCase(t,&calcCase{"zero",0,2,1})}

# 这里给出了所以的代码,方便对比查阅,感觉其中演进变化之处

执行结果如下

calculate %go test -run TestMul -v                         
=== RUN   TestMul
    calculate_test.go:12:0*2 expected 1, but 0 got
--- FAIL: TestMul (0.00s)
FAIL
exit status 1
FAIL    calculate       0.779s

发现有一个失败了,检查一下发现错误❎定位是第12行,我们回溯下
在这里插入图片描述
但是这里有三个case都调用了,具体是哪个case有问题还需要一个个排查,这样也太麻烦了吧。因此go 1.9版本中引入了t.Helper(),用于标注该函数是帮助函数,报错时将输出帮助函数调用者的信息,而不是帮助函数内部的信息。检查本机的go版本

calculate %go version
go version go1.19.6 darwin/amd64

然后修改

CreateMulTestCase

,调用

t.Helper()
funcCreateMulTestCase(c *calcCase, t *testing.T){//    t.helpers//    if ans := Mul(c.A, c.B); ans != c.Expected {//        t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)//    }

    t.Helper()
    t.Run(c.Name,func(t *testing.T){if ans :=Mul(c.A, c.B); ans != c.Expected {
            t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)}})}

输出如下所示:

calculate %go test -run TestMul -v
=== RUN   TestMul
=== RUN   TestMul/pos
=== RUN   TestMul/neg
=== RUN   TestMul/zero
    calculate_test.go:61:0*2 expected -1, but 0 got
--- FAIL: TestMul (0.00s)--- PASS: TestMul/pos (0.00s)--- PASS: TestMul/neg (0.00s)--- FAIL: TestMul/zero (0.00s)
FAIL
exit status 1
FAIL    calculate       0.397s

对于使用t.Helper(),有两点要注意:

  1. 不要返回错误, 帮助函数内部直接使用 t.Error 或 t.Fatal 即可,在用例主逻辑中不会因为太多的错误处理代码,影响可读性。
  2. 调用 t.Helper() 让报错信息更准确,有助于定位。

1.4 setup和teardown

一般我们编写自动化测试用例时,非业务检查逻辑会放置前者准备中诸如数据准备,抽象出一部分公共逻辑写在 setup 和 teardown 函数中。例如执行前需要实例化待测试的对象,如果这个对象比较复杂,很适合将这一部分逻辑提取出来;执行后,可能会做一些资源回收类的工作,例如关闭网络连接,释放文件等。标准库 testing 提供了这样的机制。

234567891011121314151617181920212223funcsetup(){
    fmt.Println("Before all tests")}functeardown(){
    fmt.Println("After all tests")}funcTest1(t *testing.T){
    fmt.Println("I'm test1")}funcTest2(t *testing.T){
    fmt.Println("I'm test2")}funcTestMain(m *testing.M){setup()
    code := m.Run()teardown()
    os.Exit(code)}
  • 在这个测试文件中,包含有2个测试用例,Test1 和 Test2。
  • 如果测试文件中包含函数 TestMain,那么生成的测试将调用 TestMain(m),而不是直接运行测试。
  • 调用 m.Run() 触发所有测试用例的执行,并使用 os.Exit() 处理返回的状态码,如果不为0,说明有用例失败。
  • 因此可以在调用 m.Run() 前后做一些额外的准备(setup)和回收(teardown)工作。

执行 go test,将会输出

$ go test
Before all tests
I'm test1
I'm test2
PASS
After all tests
ok      example 0.006s

2.性能测试

对于性能测试,Go语言标准库内置的testing测试框架提供了基准测试benchmark的能力,可以很容易的对一段代码进行性能测试。性能测试受环境的影响很大,为了保证测试的可重复性,在进行性能测试时,尽可能地保持测试环境的稳定。

  • 机器处于闲置状态,测试时不要执行其他任务,也不要和其他人共享硬件资源。
  • 机器是否关闭了节能模式,一般笔记本会默认打开这个模式,测试时关闭。
  • 避免使用虚拟机和云主机进行测试,一般情况下,为了尽可能地提高资源的利用率,虚拟机和云主机 CPU 和内存一般会超分配,超分机器的性能表现会非常地不稳定。

2.1 入门示例

还是从一个简单的示例开始介绍,使用斐波那契数列,接着上面的

calculate.go

中新增

Fib

在这里插入图片描述

funcFib(n int)int{if n ==0|| n ==1{return n
    }returnFib(n-2)+Fib(n-1)}

calculate_test.go

中实现一个

benchmark

用例,和单元测试一样,输入

test

会快捷联想出来

funcBenchmarkFib(b *testing.B){for i :=0; i < b.N; i++{Fib(30)}}
  • benchmark 和普通的单元测试用例一样,都位于 _test.go 文件中。
  • 函数名以 Benchmark 开头,参数是 b *testing.B。和普通的单元测试用例很像,单元测试函数名以 Test 开头,参数是 t *testing.T

运行示例

go test <module name>/<package name>

用来运行某个

package

内的所有测试用例。

运行当前 package 内的用例:

go test calculate

go test .

运行子 package 内的用例:

go test calculate/<package name>

go test ./<package name>

如果想递归测试当前目录下的所有的

package:go test ./...

go test calculate/...

go test

命令默认不运行

benchmark

用例的,如果我们想运行

benchmark

用例,则需要加上

-bench

参数。

calculate %go test -bench .
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-123273546075 ns/op
PASS
ok      calculate       1.887s

-bench 参数支持传入一个正则表达式,匹配到的用例才会得到执行,例如,只运行以

Fib

结尾的

benchmark
calculate %go test -bench="Fib$".
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-123253589261 ns/op
PASS
ok      calculate       2.174s

2.3 benchmark是如何工作的

benchmark

用例的参数

b *testing.B

,有个属性

b.N

表示这个用例需要运行的次数。

b.N

对于每个用例都是不一样的。

那这个值是如何决定的呢?

b.N

从 1 开始,如果该用例能够在 1s 内完成,

b.N

的值便会增加,再次执行。

b.N

的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快。我们仔细观察上述例子的输出:

BenchmarkFib-122025980669 ns/op
BenchmarkFib-12

中的 -12 即

GOMAXPROCS

,默认等于 CPU 核数。可以通过

-cpu

参数改变

GOMAXPROCS

-cpu

支持传入一个列表作为参数,例如

calculate %go test -bench="Fib$"-cpu=2,4.
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-23323557771 ns/op
BenchmarkFib-43433512955 ns/op
PASS
ok      calculate       3.460s

在这个例子中,改变

-cpu

的核数对结果几乎没有影响,因为这个

Fib

的调用是串行的。

332

3557771 ns/op

表示用例执行了

343

次,每次花费约 0.006s。总耗时比 1s 略多

3.3 提升准确度

对于性能测试来说,提升测试准确度是一个重要手段就是增加测试的次数,即所谓的常稳测试,可以使用

-benchtime

-count

,其中

-benchtime

默认为1s,可以设置指定

-benchtime

=5s`,如:

 calculate %go test -bench="Fib$"-benchtime=5s .
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-1217123466178 ns/op
PASS
ok      calculate       6.855s
实际执行的时间是 6.8s,比 benchtime 的 5s 要长,测试用例编译、执行、销毁等是需要时间的。 `-benchtime` 设置为 5s,用例执行次数也变成了原来的 5倍,每次函数调用时间仍为 0.6s,几乎没有变化。

`-benchtime` 的值除了是时间外,还可以是具体的次数。例如,执行 30 次可以用 `-benchtime=50x`
 calculate %go test -bench="Fib$"-benchtime=30x .
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-12303683029 ns/op
PASS
ok      calculate       1.113s

Fib调用了30次,花费1.1s。接着使用-count设置执行benchmark的论数,有点类似pytest中的repeat,例如进行5轮测试

 calculate %go test -bench="Fib$"-benchtime=10s -count=5.
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-1233583493746 ns/op
BenchmarkFib-1233303466845 ns/op
BenchmarkFib-1234723440367 ns/op
BenchmarkFib-1234423431086 ns/op
BenchmarkFib-1233793429828 ns/op
PASS
ok      calculate       61.065s

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

“Go Test测试教程”的评论:

还没有评论