0%

《Golang》单元测试

单元测试的重要性

单元测试是保证代码质量的重要保障,是由开发工程师编写

单元测试如果覆盖率足够高,能够大大降低线上出现问题的概率,必须得到充足的重视

有一种开发理念是:TDD(测试驱动开发 Test Drive Dev)

意思就是要完成一个特性,先把要完成的功能点整理出来,然后写上单元测试用例,最后再写功能函数,每写一个功能函数就运行相应的单元测试,直到测试通过。

Golang单元测试

Golang自带单元测试组建,主要包含3种,下面通过源码逐一做个浅析

Test

Test很简单,看看下面例子

1
2
3
4
5
6
7
8
9
10
11
import "testing"

func Add(a, b int) int {
return a + b
}

func TestTest_test(t *testing.T) {
if Add(1, 1) != 2 {
t.Fatal("测试不通过呀,1+1不等于2")
}
}

Example

Example就是写一个使用例子,然后通过判断输出来决定是否测试通过,看下面例子

1
2
3
4
5
6
7
8
9
10
11
12
13
import (
"fmt"
)

func Add(a, b int) int {
return a + b
}

func ExampleTest_test() {
fmt.Println(Add(1, 1))
// Output:
// 2
}

Benchmark

Benchmark用于做性能测试,通过大量运行你测试的方法来计算平均消耗的时间,看下面例子

1
2
3
4
5
6
7
8
9
10
11
12
13
import (
"testing"
)

func Add(a, b int) int {
return a + b
}

func BenchmarkTest_test(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 1)
}
}
  • b.N是边运行边预测的(有兴趣可以看src/testing/benchmark.go285行),每个benchmark实际上会被运行多次,b.N一次比一次大,最大不会超过1000000000,统计结果以b.N最大的那次为准,也就是最后一次为准。预测方法也非常巧妙,使用每次试运行的总时长来适当增加b.N的量(当然还有其他因素,比如b.N至少会以1.2倍速增长),使预测次数不至于太多

  • b.ResetTimer用于重置计时器,使用了这个方法意味着前面执行的代码都将不纳入性能评测范围,而是以当前开始

  • b.StopTimer用于关闭计时器,使用了这个方法意味着后面执行的代码都将不纳入性能评测范围

  • b.Cleanup用于指定一个清理函数,benchmark函数执行完后,会执行清理。前面讲到benchmark函数因为b.N的预测可能会执行多次,那么清理也可能执行多次

  • b.Run用于执行一个子benchmark测试

  • b.ReportAllocs要求报告内存分配数据

  • b.RunParallel用于并发测试,并发度默认为1,会开启GOMAXPROCS个g同时运行函数

  • b.SetParallelism用于设置并发测试的并发度。并发度设置为n,就会开启n*GOMAXPROCS个g并发测试

依赖注入

在Golang、Java这种强类型语言中,包之间的依赖是非常明确的,这就造成了相当大的耦合,对项目后期的改造维护造成相当大的困难,对单元测试也非常不利

所以需要依赖注入

做法基本就是通过参数传入所需要的依赖

比如A类依赖B类,那么初始化A的时候传入B的实例(最好传接口而不是实例,后面讲),如果使用这种方式,依赖复杂的话,会造成初始化代码非常复杂,go官方提供了wire工具https://github.com/google/wire来简化初始化代码的管理。

比如a函数依赖B类,那么B实例作为参数传入a

面向接口编程

在系统分析和架构中,分清层次和依赖关系,每个层次不是直接向其上层提供服务(即不是直接实例化在上层中),而是通过定义一组接口,仅向上层暴露其接口功能,上层对于下层仅仅是接口依赖,而不依赖具体类

面向接口编程可以最大限度降低耦合度。粒度细可以细到类之间的依赖,粗可以粗到业务层之间的依赖

面向接口编程与面向对象编程并不是冲突的,他两是相辅相成的

举个例子:

水果这一个类别中包含很多实例,比如苹果、梨等等,水果可以被吃,被吃这个动作就可以被定义成一个接口函数

人吃苹果时,就是人依赖苹果,当时人不要直接依赖苹果,而是依赖水果这个接口,这样的话人就可以吃各种水果

本篇讲的是单元测试,为什么会提到面向接口编程?

因为面向接口编程会对单元测试产生很大的帮助

当我们要测试人吃水果后的身体反应对不对,但是恰好家里没有了水果,水果没有办法或不方便提供,那就可以根据水果接口虚拟出各种水果,就可以进行测试了

接下来要讲的gomock测试框架就是帮你实现上述动作的

官方框架Gomock

Gomock框架github.com/golang/mock/gomock使用的前提是面向接口编程

Gomock先为接口生成一个mock文件,然后可以自己定义接口的实现,从而模拟出实现该接口的各种实例,即mock资源

比如为包github.com/pefish/go-mysql下的IMysql接口生成mock文件

1
mockgen github.com/pefish/go-mysql IMysql > ./mock/mock-go-mysql/mock.go

下面看一个完整案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package test

import (
go_mysql "github.com/pefish/go-mysql"
)

func Test1() ([]TestStruct, error) {
reDeposits := make([]TestStruct, 0)
err := go_mysql.MysqlInstance.Select(&reDeposits, "test", "*", map[string]interface{}{
"test_str": "haha",
})
if err != nil {
return nil, err
}
return reDeposits, nil
}

type TestStruct struct {
TestStr string `json:"test_str"`
}

我要针对Test1方法进行单元测试,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func Test_Test1(t *testing.T) {
ctrl := gomock.NewController(t) // 新建一个控制器
mysqlInstance := mock_go_mysql.NewMockIMysql(ctrl) // 使用控制器创建一个实例,还可以继续创建其他实例
mysqlInstance.EXPECT().Select(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).SetArg(0, []TestStruct{
{
TestStr: "haha",
},
}) // 自定义方法的实现
stubs := gostub.Stub(&go_mysql.MysqlInstance, mysqlInstance) // 替换全局变量。也叫打桩,使用的gostub工具
defer stubs.Reset() // 还原全局变量

results, err := Test1() // 执行测试函数
test.Equal(t, nil, err)
test.Equal(t, 1, len(results))
test.Equal(t, "haha", results[0].TestStr)
}

上面还用到了有一个打桩的工具,叫gostub,位于 github.com/prashantv/gostub

也用到了一个非常简单的断言工具,位于 github.com/pefish/go-test-assert

下面列举一下常用的函数:

  • SetArg 用于设置参数,对于传递指针类型变量而且函数内对参数更改的,mock这种函数需要用到这个
  • Do 用于做一些事情,mock一个没有返回值但是更改了某些状态(比如类中成员变量、全局变量)的函数时,需要用这个
  • DoAndReturn mock一个有返回值而且更改了某些状态的函数时,需要用这个。参数是一个函数,跟mock的函数签名一致
  • AnyTimes 用来允许mock的函数被调用多次



微信关注我,及时接收最新技术文章