go是规范大于编码的语言,只要按照规范编写测试程序就可以很方便地进行测试。
以TestXXX开始的单元测试,以BenchmarkXXX开始的性能测试,以ExampleXXX开始的实例测试。
这三种的使用比较简单,现在主要是看go test的比较复杂的场景。
子测试
子测试提供一种在一个测试函数中执行多个测试的能力。如果TestA,TestB,TestC三个测试函数,每个测试函数执行开始都要做相同的初始化工作,那么可以利用子测试将这三个测试合并到一个测试中,这样初始化的工作就只需要做一次了。
package main
import (
"testing"
"gotest"
)
func sub1(t *testing.T) {
var a = 1
var b = 2
var expected = 3
actual := gotest.Add(a, b)
if actual != expected {
t.Errorf("Add(%d, %d)=%d; expected:%d", a, b, actual, expected)
}
}
func sub2(t *testing.T) {
var a = 1
var b = 2
var expected = 3
acutal := gotest.Add(a, b)
if actual != expected {
t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expected)
}
}
func sub3(t *testing.T){
var a = 1
var b = 2
var expected = 3
actual := gotest.Add(a, b)
if actual != expected {
t.Errorf("Add(%d, %d) = %d, expected: %d", a, b, actual, expected)
}
}
func TestSub(t *testing.T) {
// setup code
t.Run("A=1", sub1)
t.Run("A=2", sub2)
t.Run("B=1", sub3)
// tear-down code
}执行子测试,就是使用t.Run方法,Run方法会启动新的协程来执行子测试,阻塞直到子测试执行结束再返回。
我们知道go test可以使用-run参数来指定要执行的测试名,上面的例子中子测试的名称就是Sub/A=1这样的命名,所以同样可以使用-run参数来过滤掉子测试。
子测试并发
子测试可以使用t.Parallel()指定并发,不过这样使用的话,就没有办法共享tear-down的代码了,因为子测试的执行顺序可能会在tear-down代码之后。
如果子测试可能并发的话,可以把子测试通过Run()再嵌套一层,Run()可以保证其下的所有子测试执行结束后再返回。
也就是说将子测试利用t.Parallel()变为并发的,然后将这些子测试再使用t.Run()嵌套一层。
package main
import (
"testing"
"gotest"
)
func sub1(t *testing.T) {
t.Parallel()
time.Sleep(3 * time.Second)
}
func sub2(t *testing.T) {
t.Parallel()
time.Sleep(1 * time.Second)
}
func sub3(t *testing.T){
t.Parallel()
time.Sleep(2 * time.Second)
}
func TestSubParallel(t *testing.T) {
// setup code
t.Run("group", func(t *testing.T) {
t.Run("sub1", sub1)
t.Run("sub2", sub2)
t.Run("sub3", sub3)
})
// tear-down code
}Main测试
子测试可以让多个测试共享setup和tear-down,但是有时希望再整个测试程序中做一些全局的setup和tear-down,这时候就需要Main测试了。
Main测试,就是声明一个func TestMain(m *testing.M),声明了这个函数之后,当前测试程序将不是直接执行各项测试,而是将测试交给TestMain调度。
示例:
func TestMain(m *testing.M) {
println("setup")
retCode := m.Run()
println("tear down")
os.Exit(retCode)
}TestMain执行的时候命令行参数还未解析,在m.Run()内部会进行解析,如果需要在此之前就处理命令行参数,那么可以使用flag.Parse()解析,不会影响m.Run()的内部。
实现原理
单元测试testing.T与性能测试testing.B类型用于控制测试的流程,考虑到二者有一定的相似性,Go实现的时候抽象出了testing.common这个基础类,在这个基础之上,实现了testing.T和testing.B
所以首先看testing.common的结构
testing.common
数据结构如下:
type common struct {
mu sync.RWMutex // 读写锁,用于控制本数据内的成员访问
output []byte // 存储当前测试产生的日志,测试结束后一并输出
w io.Writer // 子测试结束需要包日志传输到父测试的output中,通过w传输
ran bool // 表示是否已执行过
failed bool // 当前测试执行失败置为true
skipped bool // 当前测试是否已跳过
done bool // 当前测试及其子测试是否已结束
helpers map[string]struct{} //标记当前函数为help函数,记录日志时不会显示文件名和行号
chatty bool //对应命令行的-v参数,true表示打印详细日志
finished bool //当前测试结束置为true
hasSub int32 // 是否包含子测试
raceErrors int //竟态检测错误数
runner string // 执行当前测试的函数名
parent *common //父测试的指针
level int //测试嵌套的层数
creator []uintptr //测试函数调用栈
name string //记录每个测试函数名
start time.Time //记录测试开始的时间
duration time.Duration //测试所花费的时间
barrier chan bool //用于控制父测试和子测试执行
signal chan bool //通知当前测试结束
sub []*T //子测试列表
}TB接口
TB接口就是testing.T和testing.B公用的接口,定义在src/testing/testing.go下:
// TB is the interface common to T, B, and F.
type TB interface {
Cleanup(func())
Error(args ...any)
Errorf(format string, args ...any)
Fail()
FailNow()
Failed() bool
Fatal(args ...any)
Fatalf(format string, args ...any)
Helper()
Log(args ...any)
Logf(format string, args ...any)
Name() string
Setenv(key, value string)
Skip(args ...any)
SkipNow()
Skipf(format string, args ...any)
Skipped() bool
TempDir() string
// A private method to prevent users implementing the
// interface and so future additions to it will not
// violate Go 1 compatibility.
private()
}这里还定义了一个private方法,避免与用户自定义的接口类似冲突。
单元测试原理
在src/testing/testing.go:T定义了其数据结构:
type T struct {
common //就是前面的testing.common结构
isParallel bool // 是否并行
context *testContext //控制测试的并发度
}需要先了解一下testContext
type testContext struct {
match *matcher //匹配器,用于管理测试名称匹配过滤
mu sync.Mutex //互斥锁,控制并发访问
startParallel chan bool //并发控制管道,达到最大数会阻塞
running int //当前并发执行的测试个数
numWaiting int //等待并发执行的测试个数
maxParallel int //最大并发数
}使用t.Parallel()启动并发,会检测当前并发数是否达到最大值,这个检查工作就在testContext.waitParalle()首先:
func (c *testContext) waitParallel() {
c.mu.Lock()
if c.running < c.maxParallel {
c.running++
c.mu.Unlock()
return
}
c.numWaiting++
c.mu.Unlock()
<-c.startParallel
}并发测试结束则是会释放一个信号,用于启动其他等待并发测试的函数,在testContext.release()方法中:
func (c *testContext) release() {
c.mu.Lock()
if c.numWaiting == 0 {
c.running--
c.mu.Unlock()
return
}
c.numWaiting--
c.mu.Unlock()
c.startParallel <- true
}函数tRunner用于执行一个测试,大致的处理逻辑如下:
func tRunner(t *T, fn func(t *T)) {
defer func() {
t.duration += time.Since(t.start)
signal := true
t.report()
t.done = true
t.signal <- signal
}()
t.start = time.Now()
fn(t)
t.finished = true
}启动子测试的Run函数:
func (t *T) Run(name string, func(t *T)) bool {
t = &T {
common: common{
barrier: make(chan bool),
signal: make(chan bool),
name: testName,
parent: &t.common,
level: t.level + 1,
chatty: t.chatty,
},
context: t.context,
}
go tRunner(t, f)
if !<-t.signal {
runtime.Goexit()
}
return !t.failed
}每个子测试都创建一个testing.T变量,继承当前测试的部分属性,然后以新协程去执行,当前测试会在子测试结束后返回子测试的结果。
使用Parallel()启动并发测试:
func (t *T) Parallel() {
t.isParallel = true
t.duration += time.Since(t.start)
// 当前测试加入到父测试的列表中
t.parent.sub = append(t.parent.sub, t)
t.signal <- true //让父测试不必等待
<-t.parent.barrier //等待父测试启动
t.context.waitParallel() //阻塞等待并发调度
t.start = time.Now()//并发开始执行
}性能测试原理
性能测试中,最神奇的就是b.N的值,会自动调整以保证可靠的计时,那么就了解这是如何实现的吧。
testing.B的数据结构定义在src/testing/benchmark.go:B中
type B struct {
common //testing.common结构
importPath string // import path of the package containing the benchmark
context *benchContext
N int // 目标代码执行次数
previousN int // number of iterations in the previous run
previousDuration time.Duration // total duration of the previous run
benchFunc func(b *B) //性能测试函数
benchTime durationOrCountFlag //性能测试函数最少执行时间
bytes int64 //每次迭代处理的字节数
missingBytes bool // one of the subbenchmarks does not have bytes set.
timerOn bool //是否已开始计时
showAllocResult bool
result BenchmarkResult //测试结果
parallelism int // RunParallel creates parallelism*GOMAXPROCS goroutines
// The initial states of memStats.Mallocs and memStats.TotalAlloc.
startAllocs uint64 //计时开始堆中分配的对象总数
startBytes uint64 //计时开始堆中分配的字节总数
// The net total of this test after being run.
netAllocs uint64 //计时结束时,堆中增加的对象总数
netBytes uint64 //计时结束时,堆中增加的字节总数
// Extra metrics collected by ReportMetric.
extra map[string]float64
}