• Go单元测试及框架使用


    Go自带测试框架

    单元测试

    • 建议Go 语言推荐测试文件和源代码文件放在一块,测试文件以 _test.go 结尾。
    • 函数名必须以 Test 开头,后面一般跟待测试的函数名
    • 参数为 t *testing.T

    简单测试用例定义如下:

    func TestXXXX(t *testing.T) {
         // ...
     }
    
    • 1
    • 2
    • 3

    goland中,编写好方法后,右键Generate->Test for funtion, 可自动生成单元测试代码

    img

    img

    生成的代码如下:

    img

    需要在TODO里填上单元测试参数,含义如下:

    name:单元测试名称
     args:方法入参
     want:希望的出参
    
    • 1
    • 2
    • 3

    测试结果:

    img

    日志打印

    Log()打印日志
    Logf()格式化打印日志
    Error()打印错误日志
    Errorf()格式化打印错误日志
    Fatal()打印致命日志, 会直接中断当前测试方法
    Fatalf()格式化打印致命日志,会直接中断当前测试方法
    Fail()标记失败,但继续执行当前测试函数
    FailNow()失败,立即终止当前测试函数执行
    Skip()跳过当前函数,通常用于未完成的测试用例

    基准测试

    基准测试用例的定义如下:

    func BenchmarkName(b *testing.B){
         // ...
     }
    
    • 1
    • 2
    • 3
    • 函数名必须以 Benchmark 开头,后面一般跟待测试的函数名
    • 参数为 b *testing.B
    • goland中没有自动基准测试的方法,需要按照规则手动自己加

    原方法

    func sayHi(name string)  string{
       return "hi," + name
     }
    
    • 1
    • 2
    • 3

    基准测试代码

    func BenchmarkSayHi(b *testing.B) {
       for i := 0; i < b.N; i++ {
         sayHi("Max")
       }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Goland中执行基准测试

    img

    命令行中执行基准测试

    go test helloworld_test.go
    
    • 1

    结果解读

    当测试开始时,b.N的值被设置为1,执行后如果没有超过默认执行时间上限(默认为1秒),则加大b.N的值,按某种规则一直递增,直到执行时间等于或超过上限,那么就用这一次的b.N的值,做为测试的最终结果

    BenchmarkSayHi-12       81593520                14.71 ns/op
     PASS
     ok      zh.com/internal/benchmark_test  2.347s
    
    • 1
    • 2
    • 3
    • BenchmarkSayHi-12表示执行 BenchmarkSayHi 时,所用的最大P的数量为12
    • 81593520: 表示sayHi()方法在达到这个执行次数时,等于或超过了1秒
    • 14.71 ns/op: 表示每次执行sayHi()所消耗的平均执行时间
    • 2.347s:表示测试总共用时

    测试总时间的计算

    既然81593520表示1秒或大于1秒时执行的次数,那么测试总时间用时却是2.386s,超出了不少,这是为什么呢

    在测试中加入b.Log(“NNNNN:”, b.N),再执行基准测试,并加入-v,打印测试中的日志

    func BenchmarkSayHi(b *testing.B) {
        for i := 0; i < b.N; i++ {
           SayHi("Max")
       }
        b.Log("NNNNN:", b.N)
    }
    go test -v -bench=. -run=^$ gott/SayHi
     BenchmarkSayHi
         fun1_test.go:26: NNNNN: 1
         fun1_test.go:26: NNNNN: 100
         fun1_test.go:26: NNNNN: 10000
         fun1_test.go:26: NNNNN: 1000000
         fun1_test.go:26: NNNNN: 3541896
         fun1_test.go:26: NNNNN: 4832275
     BenchmarkSayHi-4         4832275               236.8 ns/op
     PASS
     ok      gott/SayHi      2.395s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可以看到b.Log(“NNNNN:”, b.N)被执行了6次,这证明了之前提到的,测试会对b.N依次递增,直到执行时间等于或超过上限。在对BenchmarkSayHi()运行基准测试时,N值依次按1,100,10000,1000000,3541896,4832275递增,直到执行次数为4832275时,执行时间等于或超过了上限。

    同时也说明BenchmarkSayHi()一共被调用了6次,每次运行BenchmarkSayHi()都要消耗一定的时间,所以测试总耗时为这6次调用时间之和,2.395s,超过了1秒

    benchtime 标记

    可以通过-benchtime标记修改默认时间上限,比如改为3秒

    go test -v -bench=. -benchtime=3s -run=^$ gott/SayHi
     goos: darwin
     goarch: amd64
     pkg: gott/SayHi
     BenchmarkSayHi
         fun1_test.go:31: NNNNN: 1
         fun1_test.go:32: /Users/ga/m/opt/go/go_root
         fun1_test.go:31: NNNNN: 100
         fun1_test.go:32: /Users/ga/m/opt/go/go_root
         fun1_test.go:31: NNNNN: 10000
         fun1_test.go:32: /Users/ga/m/opt/go/go_root
         fun1_test.go:31: NNNNN: 1000000
         fun1_test.go:32: /Users/ga/m/opt/go/go_root
         fun1_test.go:31: NNNNN: 15927812
         fun1_test.go:32: /Users/ga/m/opt/go/go_root
     BenchmarkSayHi-4    15927812         223.4 ns/op
     PASS
     ok    gott/hello  3.802s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    还可以设置具体的探索次数最大值,格式为-benchtime=Nx

    go test gott/hello -run=^$ -bench=BenchmarkHello -benchtime=50x
     goos: darwin
     goarch: amd64
     pkg: gott/hello
     BenchmarkHello-4              50              2183 ns/op
     --- BENCH: BenchmarkHello-4
         fun1_test.go:35: NNNNN: 1
         fun1_test.go:36: /Users/ga/m/opt/go/go_root
         fun1_test.go:35: NNNNN: 50
         fun1_test.go:36: /Users/ga/m/opt/go/go_root
     PASS
     ok      gott/hello      0.011s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    b.N的值被设置为50,函数运行了50次

    benchmem 标记

    可以通过-benchmem标记查看内存使用信息

    go test -bench=. -run=none -benchmem

    go test gott/hello -run=^$ -bench=BenchmarkHello -benchmem
    go test gott/hello -run=^$ -bench=BenchmarkHello -benchmem
    goos: darwin
    goarch: amd64
    pkg: gott/hello
    BenchmarkHello-4         5137456               223.1 ns/op            32 B/op          2 allocs/op
    --- BENCH: BenchmarkHello-4
    fun1_test.go:35: NNNNN: 1
    fun1_test.go:36: /Users/ga/m/opt/go/go_root
    fun1_test.go:35: NNNNN: 100
    fun1_test.go:36: /Users/ga/m/opt/go/go_root
    fun1_test.go:35: NNNNN: 10000
    fun1_test.go:36: /Users/ga/m/opt/go/go_root
    fun1_test.go:35: NNNNN: 1000000
    fun1_test.go:36: /Users/ga/m/opt/go/go_root
    fun1_test.go:35: NNNNN: 5137456
    fun1_test.go:36: /Users/ga/m/opt/go/go_root
    ... [output truncated]
    PASS
    ok      gott/hello      1.399s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 32 B/op:平均每次迭代内存分配的字节数
    • 2 allocs/op:平均每次迭代内存分配的次数

    平均每次迭代计算的依据应该使用的是 b.N=5137456迭代次数

    基准测试的用途

    一般用于对比两个不同的操作所消耗的时间,如

    • 渐近增长函数的运行时间一个函数需要1ms处理1,000个元素,处理10000或1百万将需要多少时间呢
    • I/O缓存该设置为多大基准测试可以帮助我们选择在性能达标情况下所需的最小内存
    • 确定哪种算法更好

    覆盖率测试

    运行run with coverage

    img

    结果解读

    右侧会展示覆盖率,左侧绿色为单元测试已覆盖到的代码,红色为未覆盖的代码

    img

    example测试

    样例测试比较像平时在一些算法刷题平台(比如LeetCode)的题目的一些例子,样例测试以Example打头,其逻辑也很简单,就是使用fmt.Println输出该测试用例的返回结果,然后在函数体的末尾使用如图的注释,一一对应每个fmt.Println的输出:

    img

    如果输出和注释不能对应上则不通过

    模糊测试

    go版本要求

    Fuzz模糊测试需要Go 1.18 Beta 1或以上版本的泛型功能

    测试代码

    package fuzz_test
    
    import (
    	"fmt"
    	"testing"
    	"unicode/utf8"
    )
    
    func Reverse(s string) string {
    	b := []byte(s)
    	for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
    		b[i], b[j] = b[j], b[i]
    	}
    	return string(b)
    }
    
    func FuzzReverse(f *testing.F) {
    	testcases := []string{"Hello, world", "!12345"}
    	for _, tc := range testcases {
    		f.Add(tc) // Use f.Add to provide a seed corpus
    	}
    	f.Fuzz(func(t *testing.T, orig string) {
    		rev := Reverse(orig)
    		fmt.Printf("original->:%s", orig)
    		fmt.Printf("after->:%s", rev)
    
    		doubleRev := Reverse(rev)
    		if orig != doubleRev {
    			t.Errorf("Before: %q, after: %q", orig, doubleRev)
    		}
    		if utf8.ValidString(orig) && !utf8.ValidString(rev) {
    			t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
    		}
    	})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    测试结果

    img

    第三方框架

    总体介绍
    框架名使用说明优点缺点
    testing如上go官方原生测试框架,简单好用断言不够友好,需要大量if else可以配合testify的assert使用
    testify1. 和 go test 无缝集成,直接使用该命令运行2. 支持断言,写法更简便3. 支持 mock & suite功能mock的功能不够强大,需要配合其他mock框架使用
    GoConvey1. 能够使用 go test 来运行测试2. 支持断言,写法更简便3. 支持通过浏览器查看测试结果4. 支持嵌套,可以分组1. 写法并不简便,主要多了个通过浏览器查看测试结果,个人觉得不是很有使用的必要2. 单元测试应该尽可能简单可维护,嵌套分组等功能太复杂,不利于维护
    结论

    建议采用testing+testify,goland支持自动生成testing单测模板,加上testify丰富的断言够用了

    mock框架

    golang中常用的stub/mock框架

    GoStubGomonkeyGomock
    轻量级打桩框架运行时重写可执行文件,类似热补丁官方提供的mock框架,功能强大
    支持为全局变量,函数打桩性能强大,使用方便mockgen 工具可自动生成mock代码;支持mock所有接口类型
    需要改造原函数,使用不方便;性能不强支持对变量,函数,方法打桩,支持打桩序列可以配置调用次数,调用顺序,根据入参动态返回结果等
    不是并发安全的;使用可能根据版本不同需要有些额外配置工作只支持接口级别mock,不能mock普通函数
    结论

    建议采用Gomonkey. GoStub很多功能不支持,GoMock每次编写完需要重新generate生成代码,不太方便

    其他特定领域mock工具

    框架名说明
    GoSqlMocksqlmock包,用于单测中mock db操作
    miniredis纯go实现的用于单元测试的redis server。它是一个简单易用的、基于内存的redis替代品,它具有真正的TCP接口。当我们为一些包含 Redis 操作的代码编写单元测试时可以使用它来 mock Redis 操作
    HttptestGolang官方自带,生成一个模拟的http server.主要使用的单测场景是:已经约定了接口,但是服务端还没实现

    其他

    goland中没有类似TestMe的Go单元测试插件,可以考虑实现一个

    mock工具GoMock使用

    GoMock

    gomock 是官方提供的 mock 框架,同时还提供了 mockgen 工具用来辅助生成测试代码。

    go get -u github.com/golang/mock/gomock go get -u github.com/golang/mock/mockgen

    简单的使用方法:

    // db.go
     type DB interface {
       Get(key string) (int, error)
     }
     
     func GetFromDB(db DB, key string) int {
       if value, err := db.Get(key); err == nil {
         return value
       }
     
       return -1
     }
     复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    有一个DB接口,使用mockgen产生一个mock对象

    mockgen -source=db.go -destination=db_mock.go -package=main

    下面是自动生成的代码

    // Code generated by MockGen. DO NOT EDIT.
     // Source: db.go
     
     // Package mian is a generated GoMock package.
     package mian
     
     import (
        reflect "reflect"
     
        gomock "github.com/golang/mock/gomock"
     )
     
     // MockDB is a mock of DB interface.
     type MockDB struct {
        ctrl     *gomock.Controller
        recorder *MockDBMockRecorder
     }
     
     // MockDBMockRecorder is the mock recorder for MockDB.
     type MockDBMockRecorder struct {
        mock *MockDB
     }
     
     // NewMockDB creates a new mock instance.
     func NewMockDB(ctrl *gomock.Controller) *MockDB {
        mock := &MockDB{ctrl: ctrl}
        mock.recorder = &MockDBMockRecorder{mock}
        return mock
     }
     
     // EXPECT returns an object that allows the caller to indicate expected use.
     func (m *MockDB) EXPECT() *MockDBMockRecorder {
        return m.recorder
     }
     
     // Get mocks base method.
     func (m *MockDB) Get(key string) (int, error) {
        m.ctrl.T.Helper()
        ret := m.ctrl.Call(m, "Get", key)
        ret0, _ := ret[0].(int)
        ret1, _ := ret[1].(error)
        return ret0, ret1
     }
     
     // Get indicates an expected call of Get.
     func (mr *MockDBMockRecorder) Get(key interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDB)(nil).Get), key)
     }
     复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    在测试的使用mock对象

    func TestGetFromDB(t *testing.T) {
       ctrl := gomock.NewController(t)
       defer ctrl.Finish() // 断言 DB.Get() 方法是否被调用
     
       m := NewMockDB(ctrl)
       m.EXPECT().Get(gomock.Eq("Tom")).Return(100, errors.New("not exist")) //设置期望返回结果,可以设置可调用次数times/AnyTimes
     
       if v := GetFromDB(m, "Tom"); v != -1 {
         t.Fatal("expected -1, but got", v)
       }
     }
     复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    goMock支持对特定输入打桩和对任意输入打桩(gomock.any()),可根据具体情况使用;

    实际项目中,可以用gomock来mock dao层和rpc层代码,隔离外部依赖

  • 相关阅读:
    C语言初学习——易错点合集(持续更新中)
    HOSMEL:一种面向中文的可热插拔模块化实体链接工具包
    linux安装gcc4.6.1
    【毕业设计】深度学习YOLOv5车辆颜色识别检测 - python opencv
    排序-基数排序
    食品饮料行业采购协同管理系统:优化企业采购流程效率,降低经营成本
    Arduino驱动LIS331HH三轴加速度传感器(惯性测量传感器篇)
    YOLOv5-seg数据集制作、模型训练以及TensorRT部署
    【Java】IntelliJ IDEA使用JDBC连接MySQL数据库并写入数据
    shiro篇---开启常见的注解
  • 原文地址:https://blog.csdn.net/xidianhuihui/article/details/131128806