test
0.1 · go test#
标准库内的 testing 包支持对go的自动化测试,与 go test 命令一起使用,可以自动执行符合特定函数签名的函数。
支持单元测试、性能基准测试、模糊测试,还有示例函数,格式如下:
func TestXxx(t *testing.T): 测试函数,测试程序的一些逻辑行为是否正确
func BenchmarkXxx(b *testing.B): 基准函数,测试函数的性能
func FuzzXxx(f *testing.F): 模糊测试,随机生成测试数据
func ExampleXxx(): 示例函数,为文档提供示例文档
在编写一个测试套件(一组相关的测试)时,创建以 _test.go 为后缀名的源代码文件,其包含符合以上函数签名的函数。
将这个文件放在需要测试的函数所属的相同package中,这个文件会在常规build的时候被忽略,但在 “go test” 命令执行时被包含。
总的来说,“go test” 命令会遍历*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,进行构建、运行、报告测试结果,最后清理测试中生成的临时文件。
更多细节,可以运行 “go help test” 和 “go help testflag” 命令查看。
Testing flags#
“go test” 命令可以接收对 “go test” 命令本身的标志(flags) ,也可以接收对生成的测试程序的标志(flags)。
常用的测试标志如下:
-cpu 1,2,4
设置一组GOMAXPROCS值,然后执行对应的测试。
-count n
执行测试的次数(默认为1),如果设置了 -cpu,执行对应GOMAXPROCS的测试 n 次。
-parallel n
测试函数调用 t.Parallel 时,允许并行执行,这个标志的值是允许同时运行的最大数量测试。默认为GOMAXPROCS值。
-run regexp
运行函数名匹配正则表达式的测试。
-timeout d
如果测试超过 d 秒,则panic。d为0则禁止此功能。默认为10分钟。
-v
详细输出。
-bench regexp
运行匹配正则的基准测试。默认不允许运行任何基准测试。'-bench .' 或 '-bench=.'运行所有的基准测试。
-benchtime t
运行基准测试的时间,默认为1秒。如果是Nx,表示运行基准测试N次。
-fuzz regexp
运行匹配正则的模糊测试。
-fuzztime t
运行模糊测试的时间,默认为一直执行。如果是Nx,表示运行基准测试N次。
-fuzzminimizetime t
运行模糊测试的最短时间,默认60秒。如果是Nx,表示运行基准测试N次。
-cover
开启代码覆盖率分析。
-covermode set,count,atomic
代码覆盖率分析模式,默认为set
set: bool值,这个语句运行了吗?
count: int值,这个语句运行了多少次?
atomic:int值,这个语句运行了多少次?在多线程运行情况下更准确。
-coverpkg pattern1,pattern2,pattern3
指定要覆盖测试的包,默认为所有包。
一些标志可以控制 “profiling”,同时 将程序的 “execution profile” 写出,供 “go tool pprof” 命令剖析。
-benchmem
打印内存分配统计信息。
-cpuprofile cpu.out
在程序退出之前将CPU profile写入指定的文件。
-memprofile mem.out
在所有测试通过后,将内存profile写入指定的文件。
对于一个生成的test二进制文件,以上标志可以使用 “test.” 前缀设置,比如:
pkg.test -test.v -myflag testdata -test.cpuprofile=prof.out
标志 “-args” 后的所有标志会被传递给测试程序,而不会被解释与修改。
go test -v -args -x
# 如果是执行test二进制文件,则如下
pkg.test -test.v -x
0.1.2 · func TestXxx(t *testing.T)#
func TestXxx(t *testing.T){
// ...
}
参数v用于报告测试失败和附加的日志信息
go test -v
go test -run '^TestXxx$' -v
跳过某些测试用例
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("short模式下会跳过该测试用例")
}
...
}
go test -short
Subtests#
testing.T.Run 方法可以定义子测试,不用为其单独写一个测试函数。 可以用之进行表驱动测试(Table Driven Test),也可让这些函数共用相同的setup和tear-down代码。
func TestXxx(t *testing.T) {
// <setup code>
t.Run("A=1", func(t *testing.T) { ... })
t.Run("A=2", func(t *testing.T) { ... })
t.Run("B=1", func(t *testing.T) { ... })
// <tear-down code>
}
go test -run Foo/A= # For top-level tests matching "Foo", run subtests matching "A=".
go test -run /A=1 # For all top-level tests, run subtests matching "A=1".
0.1.2.1.1 · Table Driven Test#
表格里的每一个条目都是一个完整的测试用例,包含输入和预期结果,有时还包含测试名称等附加信息,以使测试输出易于阅读。
func TestXxx(t *testing.T) {
type test struct {
input string
sep string
want []string
}
tests := []test{
{input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},
{input: "a/b/c", sep: ",", want: []string{"a/b/c"}},
{input: "abc", sep: "/", want: []string{"abc"}},
}
for _, tc := range tests {
got := Xxx(tc.input, tc.sep)
if !reflect.DeepEqual(tc.want, got) {
t.Fatalf("expected: %v, got: %v", tc.want, got)
}
}
}
0.1.2.1.2 · 并行测试
所有子测试完成,整个测试才会结束。 以下的所有子测试都并发运行。
func TestGroupedParallel(t *testing.T) {
for _, tc := range tests {
tc := tc // capture range variable
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
...
})
}
}
0.1.2.2 · pkg for test#
- stretchr/testify: testify/assert, testify/require
0.1.2.2.1 · HTTP Test#
- net/http/httptest
// gin_test.go
package httptest_demo
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_helloHandler(t *testing.T) {
// 定义两个测试用例
tests := []struct {
name string
param string
expect string
}{
{"base case", `{"name": "liwenzhou"}`, "hello liwenzhou"},
{"bad case", "", "we need a name"},
}
r := SetupRouter()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// mock一个HTTP请求
req := httptest.NewRequest(
"POST", // 请求方法
"/hello", // 请求URL
strings.NewReader(tt.param), // 请求参数
)
// mock一个响应记录器
w := httptest.NewRecorder()
// 让server端处理mock请求并记录返回的响应内容
r.ServeHTTP(w, req)
// 校验状态码是否符合预期
assert.Equal(t, http.StatusOK, w.Code)
// 解析并检验响应内容是否复合预期
var resp map[string]string
err := json.Unmarshal([]byte(w.Body.String()), &resp)
assert.Nil(t, err)
assert.Equal(t, tt.expect, resp["msg"])
})
}
}
- https://github.com/h2non/gock 对外部API进行mock,即mock指定参数返回约定好的响应内容。
https://github.com/jarcoal/httpmock
0.1.2.2.2 · MySQL Test#
0.1.2.2.3 · Redis Test#
0.1.2.2.4 · Mock && Stub#
Mock: 模拟,创建一个结构体, 满足某个外部依赖(模拟对象)的接口。采用的是接口替换的方式。 Stub: 桩,占坑的代码,桩代码给出的实现是临时性的/待编辑的。采用的是函数替代的方式。
https://stackoverflow.com/questions/3459287/whats-the-difference-between-a-mock-stub https://www.martinfowler.com/articles/mocksArentStubs.html
0.1.2.2.5 · Mock#
- https://github.com/golang/mock mock相关接口
0.1.2.2.6 · Stub#
用一些代码(桩stub)代替目标代码,通常用来屏蔽或补齐业务逻辑中的关键代码,以方便进行单元测试。
屏蔽:不想在单元测试用引入数据库连接等重资源;
补齐:依赖的上下游函数或方法还未实现;
gomock支持针对参数、返回值、调用次数、调用顺序等进行打桩操作。
- https://github.com/prashantv/gostub 支持为全局变量、函数等打桩
0.1.2.2.7 · monkey#
- https://github.com/bouk/monkey 在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现,其原理类似于热补丁。 note: monkey不支持内联函数,在测试的时候需要通过命令行参数-gcflags=-l关闭Go语言的内联优化;monkey不是线程安全的,所以不要把它用到并发的单元测试中。
- https://github.com/agiledragon/gomonkey
0.1.2.2.8 · pkg goconvey#
Convey() define scope/context/behavior/ideas So() make assertion
standard assertions assertion
custom assertions 断言是一个函数,按指定的函数签名实现一个自定义断言即可。
skip an entire scope or some assertions.
SkipConvey() replace Convey(): the func will not be running , any Convey() inside same;
use the nil instead of the an func() of Convey();
SkipSo() replace So():
FocusConvey() If the top-level Convey is changed to FocusConvey, only nested scopes that are defined with FocusConvey will be run.
go内建test的拓展,促进了go中 BDD行为驱动开发;
默认,test fail or panic 导致这个scope中的test 停止;可以传FailureContinues;
A Convey’s Reset() runs at the end of each Convey() within that same scope.
0.1.3 · func BenchmarkXxx(b *testing.B)#
func BenchmarkRandInt(b *testing.B) {
for i := 0; i < b.N; i++ {
rand.Int()
}
}
# 只运行基准测试
go test -bench=. -run=none
# 输出结果,意味着 b.N 为68453040次且每次花费时间为17.8 ns
BenchmarkRandInt-8 68453040 17.8 ns/op
如果在bench某个函数前,会花费很多时间进行一些设置,可以调用b.ResetTimer()。
func BenchmarkBigLen(b *testing.B) {
big := NewBig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
big.Len()
}
}
如果需要进行并发测试,可以使用 b.RunParallel(func(pb *testing.PB) {}) 函数
func BenchmarkTemplateParallel(b *testing.B) {
templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
b.RunParallel(func(pb *testing.PB) {
var buf bytes.Buffer
for pb.Next() {
buf.Reset()
templ.Execute(&buf, "World")
}
})
}
相关工具配合bench使用
Sub-benchmarks#
func BenchmarkAppendFloat(b *testing.B) {
benchmarks := []struct{
name string
float float64
fmt byte
prec int
bitSize int
}{
{"Decimal", 33909, 'g', -1, 64},
{"Float", 339.7784, 'g', -1, 64},
{"Exp", -5.09e75, 'g', -1, 64},
{"NegExp", -5.11e-95, 'g', -1, 64},
{"Big", 123456789123456789123456789, 'g', -1, 64},
...
}
dst := make([]byte, 30)
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
AppendFloat(dst[:0], bm.float, bm.fmt, bm.prec, bm.bitSize)
}
})
}
}
0.1.4 · func FuzzXxx(f *testing.F)#
模糊测试,是一种基于随机输入的自动化测试技术。 模糊测试不需要使用预先定义好的数据集作为程序输入,而是会通过数据构造引擎自行构造或基于开发人员提供的初始数据构造一些随机数据(称为语料 corpus),作为输入提供给程序,进而监测程序是否出现panic、断言失败、无限循环等。
https://go.dev/doc/fuzz/ https://go.googlesource.com/proposal/+/master/design/draft-fuzzing.md https://docs.google.com/document/u/1/d/1zXR-TFL3BfnceEAWytV8bnzB2Tfp6EPFinWVJ5V4QC8/pub
https://towardsdatascience.com/fuzzing-tests-in-go-96eb08b7694d
func FuzzHex(f *testing.F) {
// 提供初始的种子语料(seed corpus)
for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, in []byte) {
enc := hex.EncodeToString(in)
out, err := hex.DecodeString(enc)
if err != nil {
t.Fatalf("%v: decode: %v", in, err)
}
if !bytes.Equal(in, out) {
t.Fatalf("%v: not equal after round trip: %v", in, out)
}
})
}
种子输入可以通过调用 (*F).Add 或者将文件存储在 testdata/fuzz/<Name> (其中 <Name> 是模糊测试函数的名称), 储存目录在目标测试包中。
(*F).Add支持以下类型输入
string, []byte
int, int8, int16, int32/rune, int64
uint, uint8/byte, uint16, uint32, uint64
float32, float64
bool
Fuzzing of built-in types (e.g. simple types, maps, arrays) and types which implement the BinaryMarshaler and TextMarshaler interfaces are supported.
(*F).Fuzz的参数是一个函数,称作 fuzz target ,接收一个 *T 参数,后面是一个或多个随机的输入,随机的输入与 (*F).Add 的输入相同
如果 fuzz target 基于一个随机输入测试失败了,这个输入会被写入 testdata/fuzz/
当模糊测试未启用时,调用 fuzz target 只会使用 F.Add 注册和 testdata/fuzz/
最佳实践: 定义 fuzzing arguments,首先要想明白怎么定义 fuzzing arguments,并通过给定的 fuzzing arguments 写 fuzzing target 思考 fuzzing target 怎么写,这里的重点是怎么验证结果的正确性,因为 fuzzing arguments 是“随机”给的,所以要有个通用的结果验证方法 思考遇到失败的 case 如何打印结果,便于生成新的 unit test 根据失败的 fuzzing test 打印结果编写新的 unit test,这个新的unit test会被用来调试解决fuzzing test发现的问题,并固化下来留给CI用
0.1.5 · func ExampleXxx()#
example 函数和测试函数很想,但不是使用 “*testing.T” 报告成功或失败,而是直接打印输出到 “os.Stdout”。
如果函数体的最后一行注释以 “Output:” 开头,那么函数的输出就会这个注释进行比较。
如果最后一行注释以 “Unordered output:” 开头,那么函数的输出就会这个注释进行比较,但会忽略输出行的顺序。
example 函数无此注释,其会被编译但不会被执行;有此注释,但注释后无内容,会被编译、执行,且输出应该为空。
func ExamplePrintln() {
Println("The output of\nthis example.")
// Output: The output of
// this example.
}
func ExamplePerm() {
for _, value := range Perm(4) {
fmt.Println(value)
}
// Unordered output: 4
// 2
// 1
// 3
// 0
}
0.1.6 · test coverage#
在测试中至少被运行一次的代码占总代码的比例,一般会要求测试覆盖率达到80%左右。
go test -cover
0.1.7 · tricks#
https://zhuanlan.zhihu.com/p/335484135 https://povilasv.me/go-advanced-testing-tips-tricks/
import (
"fmt"
"path/filepath"
"runtime"
"reflect"
"testing"
)
// assert fails the test if the condition is false.
func assert(tb testing.TB, condition bool, msg string, v ...interface{}) {
if !condition {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...)
tb.FailNow()
}
}
// ok fails the test if an err is not nil.
func ok(tb testing.TB, err error) {
if err != nil {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error())
tb.FailNow()
}
}
// equals fails the test if exp is not equal to act.
func equals(tb testing.TB, exp, act interface{}) {
if !reflect.DeepEqual(exp, act) {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act)
tb.FailNow()
}
}
Use inline interfaces: 结构体内嵌接口,只要结构体值能满足接口则可以赋值; 测试的时候,可以mock一个满足接口的结构体 也就是 caller should create the interface instead of the callee providing an interface
0.2 · integration test#
https://www.ardanlabs.com/blog/2019/03/integration-testing-in-go-executing-tests-with-docker.
0.3 · testable code#
解决紧耦合 抽象为接口类型 依赖注入,在创建组件(Go 中的 struct)的时候接收它的依赖项,而不是引用外部或自行创建依赖项(https://github.com/google/wire)
0.3.0.1 · SOLID原则#
单一职责原则、开闭原则(对扩展开放,对修改关闭)、里式替换原则(在不改变程序正确性的前提下被它的子类所替换)、接口隔离原则、依赖反转原则(依赖抽象,而不是具体实现)
https://dave.cheney.net/2019/04/03/absolute-unit-test https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/revisiting-arrays-and-slices-with-generics
https://github.com/golang/go/wiki/TestComments