简单测试用例定义如下:
func TestXXXX(t *testing.T) {
// ...
}
在goland中,编写好方法后,右键Generate->Test for funtion, 可自动生成单元测试代码
生成的代码如下:
需要在TODO里填上单元测试参数,含义如下:
name:单元测试名称
args:方法入参
want:希望的出参
测试结果:
Log() | 打印日志 |
---|---|
Logf() | 格式化打印日志 |
Error() | 打印错误日志 |
Errorf() | 格式化打印错误日志 |
Fatal() | 打印致命日志, 会直接中断当前测试方法 |
Fatalf() | 格式化打印致命日志,会直接中断当前测试方法 |
Fail() | 标记失败,但继续执行当前测试函数 |
FailNow() | 失败,立即终止当前测试函数执行 |
Skip() | 跳过当前函数,通常用于未完成的测试用例 |
基准测试用例的定义如下:
func BenchmarkName(b *testing.B){
// ...
}
原方法
func sayHi(name string) string{
return "hi," + name
}
基准测试代码
func BenchmarkSayHi(b *testing.B) {
for i := 0; i < b.N; i++ {
sayHi("Max")
}
}
go test helloworld_test.go
当测试开始时,b.N的值被设置为1,执行后如果没有超过默认执行时间上限(默认为1秒),则加大b.N的值,按某种规则一直递增,直到执行时间等于或超过上限,那么就用这一次的b.N的值,做为测试的最终结果
BenchmarkSayHi-12 81593520 14.71 ns/op
PASS
ok zh.com/internal/benchmark_test 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
可以看到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标记修改默认时间上限,比如改为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
还可以设置具体的探索次数最大值,格式为-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
b.N的值被设置为50,函数运行了50次
可以通过-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
平均每次迭代计算的依据应该使用的是 b.N=5137456迭代次数
一般用于对比两个不同的操作所消耗的时间,如
运行run with coverage
右侧会展示覆盖率,左侧绿色为单元测试已覆盖到的代码,红色为未覆盖的代码
样例测试比较像平时在一些算法刷题平台(比如LeetCode)的题目的一些例子,样例测试以Example打头,其逻辑也很简单,就是使用fmt.Println输出该测试用例的返回结果,然后在函数体的末尾使用如图的注释,一一对应每个fmt.Println的输出:
如果输出和注释不能对应上则不通过
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)
}
})
}
测试结果
框架名 | 使用说明 | 优点 | 缺点 |
---|---|---|---|
testing | 如上 | go官方原生测试框架,简单好用 | 断言不够友好,需要大量if else可以配合testify的assert使用 |
testify | 1. 和 go test 无缝集成,直接使用该命令运行2. 支持断言,写法更简便3. 支持 mock & suite功能 | mock的功能不够强大,需要配合其他mock框架使用 | |
GoConvey | 1. 能够使用 go test 来运行测试2. 支持断言,写法更简便3. 支持通过浏览器查看测试结果4. 支持嵌套,可以分组 | 1. 写法并不简便,主要多了个通过浏览器查看测试结果,个人觉得不是很有使用的必要2. 单元测试应该尽可能简单可维护,嵌套分组等功能太复杂,不利于维护 | |
建议采用testing+testify,goland支持自动生成testing单测模板,加上testify丰富的断言够用了
golang中常用的stub/mock框架
GoStub | Gomonkey | Gomock |
---|---|---|
轻量级打桩框架 | 运行时重写可执行文件,类似热补丁 | 官方提供的mock框架,功能强大 |
支持为全局变量,函数打桩 | 性能强大,使用方便 | mockgen 工具可自动生成mock代码;支持mock所有接口类型 |
需要改造原函数,使用不方便;性能不强 | 支持对变量,函数,方法打桩,支持打桩序列 | 可以配置调用次数,调用顺序,根据入参动态返回结果等 |
不是并发安全的;使用可能根据版本不同需要有些额外配置工作 | 只支持接口级别mock,不能mock普通函数 |
建议采用Gomonkey. GoStub很多功能不支持,GoMock每次编写完需要重新generate生成代码,不太方便
框架名 | 说明 | |
---|---|---|
GoSqlMock | sqlmock包,用于单测中mock db操作 | |
miniredis | 纯go实现的用于单元测试的redis server。它是一个简单易用的、基于内存的redis替代品,它具有真正的TCP接口。当我们为一些包含 Redis 操作的代码编写单元测试时可以使用它来 mock Redis 操作 | |
Httptest | Golang官方自带,生成一个模拟的http server.主要使用的单测场景是:已经约定了接口,但是服务端还没实现 |
goland中没有类似TestMe的Go单元测试插件,可以考虑实现一个
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
}
复制代码
有一个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)
}
复制代码
在测试的使用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)
}
}
复制代码
goMock支持对特定输入打桩和对任意输入打桩(gomock.any()),可根据具体情况使用;
实际项目中,可以用gomock来mock dao层和rpc层代码,隔离外部依赖