系统学习、总结go中的测试。

测试金字塔

尽可能地多做单元测试 和 集成测试

尽可能地少做 组件测试、端到端测试 和 探索性测试

金字塔里,越往下速度越快且越稳定,那么就可以频繁执行,反正执行一次也花不了多久时间,开发人员还可以知道我的代码有没有影响到其他模块。越往上则速度越慢且越不稳定,跑一次要N久,开发人员往往会觉得还是先继续开发吧,到时候出了bug再说,我可不想加班等测试结果。

go中的测试

  • 测试函数
  • 基准测试
  • 示例测试
  • 模糊测试 (1.18增加)

go test 工具

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

类型 格式 作用
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能
示例函数 函数名前缀为Example 为文档提供示例文档

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

Golang单元测试对文件名和方法名,参数都有很严格的要求。

1    1、文件名必须以xx_test.go命名
2    2、方法必须是Test[^a-z]开头,否则go test 会直接跳过测试不执行
3    3、方法参数必须 t *testing.T
4    4、使用go test执行单元测试

go test的参数解读:

go test是go语言自带的测试工具,其中包含的是两类,单元测试和性能测试

通过go help test可以看到go test的使用说明:

格式形如: go test [-c] [-i] [build flags] [packages] [flags for test binary]

参数解读:

-c : 编译go test成为可执行的二进制文件,但是不运行测试。

-i : 安装测试包依赖的package,但是不运行测试。

关于build flags,调用go help build,这些是编译运行过程中需要使用到的参数,一般设置为空

关于packages,调用go help packages,这些是关于包的管理,一般设置为空

关于flags for test binary,调用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 : 程序运行在哪些CPU上面,使用二进制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一个道理

-test.short : 将那些运行时间较长的测试用例运行时间缩短

-test -cover 代码覆盖率

Example

1 go test -v  -timeout 30s pages_test.go -run ^TestAllPages
2 go test --bench=. 0016atomicvsrwmutex_test.go  
3 go test ./tests -v -count=1  //测试会有缓存,清楚缓存
4  		Go 为了提高测试的性能,会对包的编译后的测试代码进行缓存。
5  		一般常见的,也是官方推荐的清除缓存的方式是使用 -count 参数。
6  		此参数一般用以设置测试运行的次数,如果设置为 2 的话就会运行测试两次。

测试函数的格式

每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:

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

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,举几个例子:\

1func TestAdd(t *testing.T){ ... }
2func TestSum(t *testing.T){ ... }
3func TestLog(t *testing.T){ ... }

其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:

 1func (c *T) Error(args ...interface{}) // 等价 Log(args ...interface{}) +  Fail()   
 2func (c *T) Errorf(format string, args ...interface{}) //等价Logf(format string, args ...interface{}) +    Fail()   
 3func (c *T) Fail()   //标记测试函数为失败,然后继续执行(剩下的测试)。
 4func (c *T) FailNow() //标记测试函数为失败并中止执行;文件中别的测试也被略过,继续执行下一个文件。
 5func (c *T) Failed() bool
 6func (c *T) Fatal(args ...interface{}) //先执行Log() 再执行 FailNow()
 7func (c *T) Fatalf(format string, args ...interface{})
 8func (c *T) Log(args ...interface{}) //args 被用默认的格式格式化并打印到错误日志中。
 9func (c *T) Logf(format string, args ...interface{})
10func (c *T) Name() string
11func (t *T) Parallel()
12func (t *T) Run(name string, f func(t *T)) bool
13func (c *T) Skip(args ...interface{})
14func (c *T) SkipNow() // 必须写在函数第一行,跳过这个测试
15func (c *T) Skipf(format string, args ...interface{})
16func (c *T) Skipped() bool
17
18// 获取测试名称
19method (*T) Name() string
20// 打印日志
21method (*T) Log(args ...interface{})
22// 打印日志,支持 Printf 格式化打印
23method (*T) Logf(format string, args ...interface{})
24// 反馈测试失败,但不退出测试,继续执行
25method (*T) Fail()
26// 反馈测试失败,立刻退出测试
27method (*T) FailNow()
28// 反馈测试失败,打印错误
29method (*T) Error(args ...interface{})
30// 反馈测试失败,打印错误,支持 Printf 的格式化规则
31method (*T) Errorf(format string, args ...interface{})
32// 检测是否已经发生过错误
33method (*T) Failed() bool
34// 相当于 Error + FailNow,表示这是非常严重的错误,打印信息结束需立刻退出。
35method (*T) Fatal(args ...interface{})
36// 相当于 Errorf + FailNow,与 Fatal 类似,区别在于支持 Printf 格式化打印信息;
37method (*T) Fatalf(format string, args ...interface{})
38// 跳出测试,从调用 SkipNow 退出,如果之前有错误依然提示测试报错
39method (*T) SkipNow()
40// 相当于 Log 和 SkipNow 的组合
41method (*T) Skip(args ...interface{})
42// 与Skip,相当于 Logf 和 SkipNow 的组合,区别在于支持 Printf 格式化打印
43method (*T) Skipf(format string, args ...interface{})
44// 用于标记调用函数为 helper 函数,打印文件信息或日志,不会追溯该函数。
45method (*T) Helper()
46// 标记测试函数可并行执行,这个并行执行仅仅指的是与其他测试函数并行,相同测试不会并行。
47method (*T) Parallel()
48// 可用于执行子测试
49method (*T) Run(name string, f func(t *T)) bool

如果你想在测试失败的同时打印失败测试日志,那么可以直接调用t.Error方法或者t.Errorf方法。

前者相当于t.Log方法和t.Fail方法的连续调用,而后者也与之类似,只不过它相当于先调用了t.Logf方法。

t.Run来执行subtests可以做到控制test输出以及test的顺序

 1package test
 2import (
 3		"testing"
 4  	"fmt"
 5)
 6func TestPrint(t *test.T){
 7  t.Run("a1",func(t *testing.T){fmt.Println("a1")})
 8  t.Run("a2",func(t *testing.T){fmt.Println("a2")})
 9  t.Run("a3",func(t *testing.T){fmt.Println("a3")})
10}

使用TestMain 作为初始化test,并且使用m.Run()来调用其他tests,可以完成一些需要初始化操作的testing,比如数据库连接,文件打开,rest服务登录等

1➜  tmp git:(master) ✗ ls
2tmp.go      tmp_test.go
1cd tmp 
2go test -v tmp_test.go tmp.go

tmp_test.go

 1package tmp
 2
 3import (
 4	"fmt"
 5	"testing"
 6)
 7
 8func testPrint1(t *testing.T) {
 9	//t.SkipNow()
10	t.Log("testprint1 begin")
11	res := Print()
12	if res != 1 {
13		t.Errorf("testprint1 return error")
14	}
15	t.Log("testprint1 end")
16}
17func testPrint2(t *testing.T) {
18	//t.SkipNow()
19	t.Log("testprint2 begin")
20	res := Print()
21	if res != 1 {
22		t.Errorf("testprint2 return error")
23	}
24	t.Log("testprint2 end")
25}
26
27//第二执行
28func TestAll(t *testing.T) {
29	//第三执行
30	t.Run("print1", testPrint1)
31	//第四执行
32	t.Run("print2", testPrint2)
33}
34
35//最先执行
36func TestMain(m *testing.M) {
37	fmt.Println("test begins")
38	m.Run()
39}

tmp.go

1package tmp
2
3import "fmt"
4
5func Print() int {
6	fmt.Println("tmp")
7	return 1
8}

benchmark基准测试

  • benchmark函数一般以Benchmark开头
  • benchmark的case 一般回跑b.N 次,而且每次执行都会如此
  • 在执行过程中会根据实际case的执行时间是否稳定增加b.N的次数以达到稳态
1go test -bench=. 
2如果其他test函数没有错误,只会跑带Benchmark的case
3benchmark函数还是受到 TestMain的m.Run()控制
1func BenchmarkFibonacci(b *testing.B){
2	n :=10
3	b.ReportAllocs() //开启内存统计
4	b.ResetTimer() // 重制计时器
5	for i:=0;i<b.N;i++{
6	     Fibonacci(n)	
7    }
8}

并发基准测试 RunParaller会创建很多goroutine,并分配b.N 分配给这些goroutine执行

1func BenchmarkFibonacciRunParallel(b *testing.B){
2	 n :=10
3	 b.RunParaller(func(pb *testing.PB){
4	     for pb.Next(){
5            Fibonacci(n)
6     }	 
7    })
8}
 1package print
 2
 3import "strings"
 4
 5func Print() int {
 6	var age int
 7	age = 20
 8	return age
 9}
10
11
12
13func Split(s, sep string) (result []string) {
14	i := strings.Index(s, sep)
15
16	for i > -1 {
17		result = append(result, s[:i])
18		s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
19		i = strings.Index(s, sep)
20	}
21	result = append(result, s)
22	return
23}
 1package test
 2
 3import (
 4	"fmt"
 5	"go-im/print"
 6	"testing"
 7)
 8
 9func BenchmarkPrint(b *testing.B) {
10	fmt.Println("begin")
11	for i := 0; i < b.N; i++ {
12		print.Split("枯藤老树昏鸦", "老")
13	}
14}

go test -bench=Print

go test -bench=.

1# go test -bench=Print
2
3BenchmarkPrint-8         8189996               146 ns/op
4PASS
5ok      go-im/test      1.368s

其中BenchmarkSplit-8表示对Split函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要。8189996和146/op表示每次调用Split函数耗时146ns,这个结果是8189996次调用的平均值。

我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。

1#go test -bench=Print -benchmem
2
3goos: darwin
4goarch: amd64
5pkg: go-im/test
6BenchmarkPrint-8         8023243               146 ns/op              48 B/op          2 allocs/op
7PASS
8ok      go-im/test      1.353s

其中,48 B/op表示每次操作内存分配了48字节,2allocs/op则表示每次操作进行了2次内存分配。

以下来自 郝林36讲第24讲中的内容

1$ go test puzzlers/article20/q2
2ok   puzzlers/article20/q2 (cached)

可以看到,结果最右边的不再是测试耗时,而是(cached)。这表明,由于测试代码与被测代码都没有任何变动,所以go test命令直接把之前缓存测试成功的结果打印出来了。

go 命令通常会缓存程序构建的结果,以便在将来的构建中重用。我们可以通过运行go env GOCACHE命令来查看缓存目录的路径。缓存的数据总是能够正确地反映出当时的各种源码文件、构建环境、编译器选项等等的真实情况。

一旦有任何变动,缓存数据就会失效,go 命令就会再次真正地执行操作。所以我们并不用担心打印出的缓存数据不是实时的结果。go 命令会定期地删除最近未使用的缓存数据,但是,如果你想手动删除所有的缓存数据,运行一下go clean -cache命令就好了。

对于测试成功的结果,go 命令也是会缓存的。运行go clean -testcache将会删除所有的测试结果缓存。不过,这样做肯定不会删除任何构建结果缓存。

此外,设置环境变量GODEBUG的值也可以稍稍地改变 go 命令的缓存行为。比如,设置值为gocacheverify=1将会导致 go 命令绕过任何的缓存数据,而真正地执行操作并重新生成所有结果,然后再去检查新的结果与现有的缓存数据是否一致。

总之,我们并不用在意缓存数据的存在,因为它们肯定不会妨碍go test命令打印正确的测试结果。

1$ go test -bench=. -run=^$ puzzlers/article20/q3
2goos: darwin
3goarch: amd64
4pkg: puzzlers/article20/q3
5BenchmarkGetPrimes-8      500000       2314 ns/op
6PASS
7ok   puzzlers/article20/q3 1.192s

我在运行go test命令的时候加了两个标记。第一个标记及其值为-bench=.,只有有了这个标记,命令才会进行性能测试。该标记的值.表明需要执行任意名称的性能测试函数,当然了,函数名称还是要符合 Go 程序测试的基本规则的。

第二个标记及其值是-run=^$,这个标记用于表明需要执行哪些功能测试函数,这同样也是以函数名称为依据的。该标记的值^$意味着:只执行名称为空的功能测试函数,换句话说,不执行任何功能测试函数。

你可能已经看出来了,这两个标记的值都是正则表达式。实际上,它们只能以正则表达式为值。此外,如果运行go test命令的时候不加-run标记,那么就会使它执行被测代码包中的所有功能测试函数。

再来看测试结果,重点说一下倒数第三行的内容。BenchmarkGetPrimes-8被称为单个性能测试的名称,它表示命令执行了性能测试函数BenchmarkGetPrimes,并且当时所用的最大 P 数量为8

最大 P 数量相当于可以同时运行 goroutine 的逻辑 CPU 的最大个数。这里的逻辑 CPU,也可以被称为 CPU 核心,但它并不等同于计算机中真正的 CPU 核心,只是 Go 语言运行时系统内部的一个概念,代表着它同时运行 goroutine 的能力。

顺便说一句,一台计算机的 CPU 核心的个数,意味着它能在同一时刻执行多少条程序指令,代表着它并行处理程序指令的能力。

我们可以通过调用 runtime.GOMAXPROCS函数改变最大 P 数量,也可以在运行go test命令时,加入标记-cpu来设置一个最大 P 数量的列表,以供命令在多次测试时使用。

至于怎样使用这个标记,以及go test命令执行的测试流程,会因此做出怎样的改变,我们在下一篇文章中再讨论。

在性能测试名称右边的是,go test命令最后一次执行性能测试函数(即BenchmarkGetPrimes函数)的时候,被测函数(即GetPrimes函数)被执行的实际次数。这是什么意思呢?

go test命令在执行性能测试函数的时候会给它一个正整数,若该测试函数的唯一参数的名称为b,则该正整数就由b.N代表。我们应该在测试函数中配合着编写代码,比如:

1for i := 0; i < b.N; i++ {
2 GetPrimes(1000)
3}

我在一个会迭代b.N次的循环中调用了GetPrimes函数,并给予它参数值1000go test命令会先尝试把b.N设置为1,然后执行测试函数。

如果测试函数的执行时间没有超过上限,此上限默认为 1 秒,那么命令就会改大b.N的值,然后再次执行测试函数,如此往复,直到这个时间大于或等于上限为止。

当某次执行的时间大于或等于上限时,我们就说这是命令此次对该测试函数的最后一次执行。这时的b.N的值就会被包含在测试结果中,也就是上述测试结果中的500000

我们可以简称该值为执行次数,但要注意,它指的是被测函数的执行次数,而不是性能测试函数的执行次数。

最后再看这个执行次数的右边,2314 ns/op表明单次执行GetPrimes函数的平均耗时为2314纳秒。这其实就是通过将最后一次执行测试函数时的执行时间,除以(被测函数的)执行次数而得出的。

如何写出方便测试的代码

将依赖的 通过初始化的时候,依赖注入的方式。 这样可以通过mock操作,进行测试。

别注入具体的结构体,如gorm.db ,这样的话,测试层必须是gorm.db了。 合理的可以是注入 一个data接口层,这个接口有获得数据的方法。

那么你无论用gorm.db 去获得数据或者mock生成数据去实现这个接口,都能注入进去, 也方便测试层使用mock数据的方式, 进行测试。

gomock进行测试

gomock可以利用生成工具,快速生成mock代码。

安装: go get -u github.com/golang/mock/gomock

go install github.com/golang/mock/mockgen

mockgen -source mock/user.go -destination mock/mock/user.go -package mock

 1package mock
 2import "context"
 3/*
 4gomock 测试
 5
 6*/
 7type User struct {
 8	Mobile   string
 9	Password string
10	NickName string
11}
12type UserServer struct {
13	Db UserData
14}
15func (us *UserServer) GetUserByMobile(ctx context.Context, mobile string) (User, error) {
16	user, err := us.Db.GetUserByMobile(ctx, mobile)
17	if err != nil {
18		return User{}, err
19	}
20	if user.NickName == "bobby18" {
21		user.NickName = "bobby17"
22	}
23	return user, nil
24}
25type UserData interface {
26	GetUserByMobile(ctx context.Context, mobile string) (User, error)
27}

生成的代码

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

测试代码

 1package mock
 2
 3import (
 4	"context"
 5	"fmt"
 6	"github.com/golang/mock/gomock"
 7	"gotool/mock"
 8	"testing"
 9)
10
11func TestGetUserByMobile(t *testing.T) {
12	//mock 准备工作
13	ctrl := gomock.NewController(t)
14	defer ctrl.Finish()
15
16	mockUserData := NewMockUserData(ctrl)
17	//mock数据, expect()期望 返回的值  档mobile=18的时候 返回nickname是crmao1的user,  mock 的是 service中的db。 
18	mockUserData.EXPECT().GetUserByMobile(gomock.Any(), "18").Return(mock.User{
19		NickName: "crmao1",
20	}, nil)
21
22	//实际调用过程
23	userServer := mock.UserServer{
24		Db: mockUserData, //  mockUserData 就是mock了 db 调用 GetUserByMobile的过程。
25	}
26	user, err := userServer.GetUserByMobile(context.Background(), "18")
27
28	//判断正确与否
29	if err != nil {
30		t.Errorf("error: %v", err)
31	}
32	fmt.Println(user)
33	if user.NickName != "crmao2" {
34		t.Errorf("error: %v", err)
35	}
36}

go-sqlmock对gorm进行mock

 1package mock
 2
 3import "context"
 4
 5/*
 6gomock 测试
 7安装:
 8	go get -u github.com/golang/mock/gomock
 9
10	go install github.com/golang/mock/mockgen
11*/
12
13type User struct {
14	Mobile   string
15	Password string
16	NickName string
17}
18
19type UserServer struct {
20	Db UserData
21}
22
23func (us *UserServer) GetUserByMobile(ctx context.Context, mobile string) (User, error) {
24	//返回crmao1 ,mock结果是crmao1
25	user, err := us.Db.GetUserByMobile(ctx, mobile)
26	if err != nil {
27		return User{}, err
28	}
29	if user.NickName == "crmao1" {
30		//最终是crmao2
31		user.NickName = "crmao2"
32	}
33	return user, nil
34}
35
36type UserData interface {
37	GetUserByMobile(ctx context.Context, mobile string) (User, error)
38}
 1package mysql
 2
 3import (
 4	"context"
 5
 6	"gorm.io/gorm"
 7	"gotool/mock"
 8)
 9
10type user struct {
11	db *gorm.DB
12}
13
14func NewUser(db *gorm.DB) *user {
15	return &user{db: db}
16}
17
18func (u *user) GetUserByMobile(ctx context.Context, mobile string) (mock.User, error) {
19	var user mock.User
20	_ = u.db.Where(&mock.User{Mobile: mobile}).First(&user)
21	return user, nil
22}
23
24var _ mock.UserData = &user{}
 1package mysql
 2
 3import (
 4	"context"
 5	"regexp"
 6
 7	"database/sql"
 8	"gorm.io/driver/mysql"
 9	"gorm.io/gorm"
10	"testing"
11
12	"github.com/DATA-DOG/go-sqlmock"
13	"github.com/stretchr/testify/assert"
14	umock "gotool/mock"
15)
16
17func TestGetUserByMobile(t *testing.T) {
18	//注入,模拟出一个db。
19	db, mock, err := sqlmock.New()
20	if err != nil {
21		t.Fatalf("init sqlmock: %v", err)
22	}
23
24	defer func(db *sql.DB) {
25		err := db.Close()
26		if err != nil {
27			t.Fatalf("close sqlmock: %v", err)
28		}
29	}(db)
30	gormDB, err := gorm.Open(mysql.New(mysql.Config{
31		SkipInitializeWithVersion: true,
32		Conn:                      db,
33	}), &gorm.Config{})
34	if err != nil {
35		t.Fatalf("open gorm: %v", err)
36	}
37	mobile := "18"
38	//期望, 执行这个sql语句的时候, 参数是 mobile 返回字段切片, 如果是一行,他会只取一行
39	mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE `users`.`mobile` = ? ORDER BY `users`.`mobile` LIMIT 1")).
40		WithArgs(mobile).WillReturnRows(
41		sqlmock.NewRows([]string{"mobile", "password", "nick_name"}).AddRow("18758327777", "123456", "crmao"))
42	//也要进行关闭 
43	mock.ExpectClose()
44	//调用
45	userData := NewUser(gormDB)
46	user, err := userData.GetUserByMobile(context.Background(), mobile)
47	assert.Nil(t, err)
48
49	expUser := umock.User{
50		Mobile:   "18758327777",
51		Password: "123456",
52		NickName: "crmao",
53	}
54	assert.Equal(t, expUser, user)
55	/*
56		fake 测试
57		grpc 服务, rocketmq, kafka,
58	*/
59}

go fuzz 模糊测试

单元测试有局限性, 每个测试输入必须由开发者添加到单元测试用例, 更进一步还是一个人工的check过程。

fuzzing优点 就是可以基于开发者代码中指定的测试输入作为基础数据,进一步自动生成随机测试数据,用来发现指定测试输入没有覆盖到的边界情况

如果要基于种子语料库生成随机测试数据用于模糊测试,需要给go test命令加上 -fuzz=Fuzz

注意:go test默认会执行所有以TestXxx开头的单元测试函数和以FuzzXxx开头的模糊测试函数,默认不运行以BenchmarkXxx开头的性能测试函数,如果我们想运行 benchmark用例,则需要加上 -bench 参数。

不要把单元和模糊测试做对比,他们是互补的,某些核心函数我们可以同时将单元测试和模糊加上保证代码的正确性

testdata目录go的一种惯例, 如果我们的测试用例有外部输入数据,我们就将数据放到testdata,这个目录会被go test命令扫描到, 这样我们就不用关心路径问题 如果发生错误会生成testdata目录。

cd 到目录 执行

1# 三个命令都行
2go test
3go test -fuzz=Fuzz
4go test -run fuzz_test.go 
 1package fuzz
 2
 3import (
 4   "fmt"
 5   "github.com/stretchr/testify/assert"
 6   "testing"
 7   "unicode/utf8"
 8)
 9
10/*
11模糊测试,单元测试有局限性, 每个测试输入必须由开发者添加到单元测试用例, 更进一步
12还是一个人工的check过程
13fuzzing优点 就是可以基于开发者代码中指定的测试输入作为基础数据,进一步自动生成随机测试数据,用来发现指定测试输入没有覆盖到的边界情况
14*/
15
16func Reverse(s string) (string, error) {
17   //没有考虑到非法的unicode编码
18   //q 代表该值对应的单引号括起来的go语法字符字面值
19   if !utf8.ValidString(s) {
20   	return "", fmt.Errorf("invalid utf8: %q", s)
21   }
22   b := []rune(s)
23   for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
24   	b[i], b[j] = b[j], b[i]
25   }
26   return string(b), nil
27}
28
29//TDD
30/*
31当涉及业务相关的单元测试, 此时
32
33用例复杂, 输入可能是多层嵌套的struct, 某一层的某个变量的值影响输出
34用例数量多, 某个函数可能有5个以上的用例
35由于table-driven的表达能力有限:
36
37如何复用输入结构体? (当前情况开发会复制粘贴过去改)
38name过于简单不被重视, 导致单元测试失败时难以快速阅读
39
40最终带来的问题是:
41
42单个用例构造复杂, 可能是十几行甚至是几十行;
43用例和用例之间没有复用, 基本基于复制粘贴;
44难以区分用例之间的差异
45用例过多导致难以维护, 不能明确知道每个用例的目的, 用例和用例之间的差别;
46单个测试集过大, 可能有几百行测试代码;
47BDD
48goconvey ginkgo
49*/
50
51func TestReverse(t *testing.T) {
52   testcases := []struct {
53   	in, want string
54   }{
55   	{"hello", "olleh"}, //基本
56   	{"a", "a"},         //边界
57   	{" ", " "},         //特殊
58   }
59   for _, c := range testcases {
60   	rev, _ := Reverse(c.in)
61   	assert.Equal(t, c.want, rev)
62   }
63}
64
65func FuzzReverse(f *testing.F) {
66   testcases := []string{"Hello", "a", " ", "!#j"}
67   for _, c := range testcases {
68   	f.Add(c) //提供种子语料库
69   }
70   f.Fuzz(func(t *testing.T, orig string) {
71   	rev, err := Reverse(orig)
72   	if err != nil {
73   		return
74   	}
75   	//没有输入的,也有无法预期的输出, 模糊测试的缺点非常明显,就是几乎无法通过equal去判断, 只能通过比较字符串的长度来判断
76   	//如果要基于种子语料库生成随机测试数据用于模糊测试,需要给go test命令加上 -fuzz=Fuzz
77   	assert.Equal(t, len(orig), len(rev))
78   	double, err := Reverse(rev)
79   	if err != nil {
80   		return
81   	}
82   	assert.Equal(t, orig, double) //技巧
83
84   	//另一种技巧,这个技巧说实话没有什么说服力
85   	if utf8.ValidString(orig) && !utf8.ValidString(rev) {
86   		t.Errorf("invalid utf8: %q", rev)
87   	}
88   })
89}
90
91//不要把单元和模糊测试做对比,他们是互补的,某些核心函数我们可以同时将单元测试和模糊加上保证代码的正确性
92//testdata目录go的一种惯例, 如果我们的测试用例有外部输入数据,我们就将数据放到testdata,这个目录会被go test命令扫描到, 这样我们就不用关心路径问题

使用gomonkey 进行测试。

https://github.com/agiledragon/gomonkey

快速mock一个方法,也可以是结构体方法,变量 。

cd 到代码目录执行

go test -v -run TestCompute$ -gcflags=all=-l

要加-gcflags=all=-l参数 不然他还是走的是老代码没走mock 的函数逻辑

money_test.go

 1package monkey
 2
 3import (
 4	"github.com/agiledragon/gomonkey/v2"
 5	"reflect"
 6	"testing"
 7)
 8
 9// mock 一个函数, mock 范围更广, 而且不需要事先生成代码, 可以结合自己的需求使用。
10func TestCompute(t *testing.T) {
11	//动态的补丁技术, 让networkCompute 这个函数 返回一个值, 现在是给他固定2,那么算是mock好了一个函数了。
12	patches := gomonkey.ApplyFunc(networkCompute, func(a, b int) (int, error) {
13		return 2, nil
14	})
15	defer patches.Reset()
16	// Compute-》调用 networkCompute会是上面mock 的函数。
17	sum, err := Compute(1, 2)
18	if err != nil {
19		t.Error(err)
20	}
21	if sum != 4 {
22		t.Errorf("sum is %d, want 3", sum)
23	}
24}
25
26func TestCompute2(t *testing.T) {
27	//动态的补丁技术
28	var c *Computer
29	patches := gomonkey.ApplyMethod(reflect.TypeOf(c), "NetworkCompute", func(_ *Computer, a, b int) (int, error) {
30		return 2, nil
31	})
32	defer patches.Reset()
33
34	c = &Computer{}
35	sum, err := c.Compute(1, 2)
36	if err != nil {
37		t.Error(err)
38	}
39	if sum != 3 {
40		t.Errorf("sum is %d, want 3", sum)
41	}
42}
43
44var num = 10
45
46func TestGlobalVar(t *testing.T) {
47
48	patches := gomonkey.ApplyGlobalVar(&num, 12)
49	defer patches.Reset()
50
51	if num != 10 {
52		t.Errorf("expected %v, got: %v", 10, num)
53	}
54}

monkey.go

 1package monkey
 2
 3type Computer struct {
 4}
 5
 6func (c *Computer) NetworkCompute(a, b int) (int, error) {
 7	sum := a + b
 8	return sum, nil
 9}
10
11func (c *Computer) Compute(a, b int) (int, error) {
12	sum, err := c.NetworkCompute(a, b)
13	/*
14		业务逻辑
15	*/
16	return sum, err
17}
18
19func networkCompute(a, b int) (int, error) {
20	c := a + b
21	return c, nil
22}
23
24func Compute(a, b int) (int, error) {
25	sum, err := networkCompute(a, b)
26	/*
27		业务逻辑
28	*/
29	return sum, err
30}

ginkgo 测试框架入门

https://ke-chain.github.io/ginkgodoc/

goland 安装 ginkgo插件

 1package ginkgo
 2
 3import (
 4	"github.com/agiledragon/gomonkey/v2"
 5	"github.com/golang/mock/gomock"
 6	. "github.com/onsi/ginkgo"
 7	. "github.com/onsi/gomega"
 8	"github.com/stretchr/testify/assert"
 9	"testing"
10)
11
12func TestBooks(t *testing.T) {
13	RegisterFailHandler(Fail)
14	RunSpecs(t, "Books Suite")
15}
16
17// 并不一定需要每个测试用例都这么写, 对于核心的函数或者核心的业务逻辑我们建议设计好的测试用例
18var _ = Describe("Books", func() {
19	var (
20		longBook  string
21		shortBook string
22
23		pathches *gomonkey.Patches
24		ctl      *gomock.Controller
25	)
26	BeforeEach(func() {
27		longBook = "long"
28		shortBook = "short"
29
30		ctl = gomock.NewController(GinkgoT())
31	})
32
33	AfterEach(func() {
34		longBook = ""
35		shortBook = ""
36
37		ctl.Finish()
38		pathches.Reset()
39	})
40
41	Describe("Add Books", func() {
42		It("should be able to add a book", func() {
43			//调用AddBook方法,并传入参数,期望返回的结果为true
44			assert.Equal(GinkgoT(), "long", longBook)
45		})
46		It("should not be able to add a book", func() {
47			//调用AddBook方法,并传入参数,期望返回的结果为true
48			assert.Equal(GinkgoT(), "short", shortBook)
49		})
50	})
51
52	Describe("Delete Books", func() {
53
54	})
55})