Go-知识测试-工作机制
- 生成test的main
- test的main如何启动case
- [单元测试 runTests](#单元测试 runTests)
- [示例测试 runExamples](#示例测试 runExamples)
- [性能测试 runBenchmarks](#性能测试 runBenchmarks)
在 Go 语言的源码中,go test 命令的实现主要在 src/cmd/go/internal/test 包中。当你运行 go test 命令时,Go 的命令行工具会调用这个包中的代码来执行测试。
以下是 go test 命令的大致执行流程:
- 首先,go test 命令会解析命令行参数,获取需要测试的包和测试选项。
- 然后,go test 命令会构建一个测试的二进制文件。这个二进制文件包含了需要测试的包和测试用例,以及测试用例的运行环境和测试框架。
- 接着,go test 命令会启动这个二进制文件,并将命令行参数传递给它。这个二进制文件会运行测试用例,并将测试结果输出到标准输出。
- 最后,go test 命令会读取这个二进制文件的输出,解析测试结果,并将测试结果显示给用户。
在 src/cmd/go/internal/test 包中,runTest 函数是 go test 命令的主要入口点。这个函数负责解析命令行参数,构建测试的二进制文件,启动这个二进制文件,以及读取和解析测试结果。
在 runTest 函数中,runTest 函数会调用 load.TestPackagesFor 函数来获取需要测试的包,然后调用 builder.runTest 函数来构建和运行测试的二进制文件。builder.runTest 函数会调用 builder.runOut 函数来启动这个二进制文件,并将这个二进制文件的输出连接到 go test 命令的标准输出。
在 builder.runTest 函数中,builder.runTest 函数会调用 builder.compile 函数来编译需要测试的包,然后调用 builder.link 函数来链接这个包和测试框架,生成测试的二进制文件。
生成test的main
详细的来说:
首先执行 go test
命令,是一个内部命令,在源码的cmd/go
下
在这里有个main入口
在main函数里面执行 invoke 函数
在invoke里面执行Run
针对 go test
执行是初始化的test命令
在test中执行的是runTest
runTest的内容如下
会解析入参等
然后会执行
在builderTest中,构建test程序
在load包中打包
为什么go的测试都是
_test
结尾呢?
在打包test的时候,会将 path_test 也加入
针对test的程序,会构造一个main入口
真正的go test main 生成
使用模板生成
其中 testmainTmpl 是一个模板
也是有一个main入口
在go 1.17 中,渲染后的代码如下
Go
package main
import (
"os"
"testing"
"testing/internal/testdeps"
_test "mypackage"
)
var tests = []testing.InternalTest{
{"TestFunc1", _test.TestFunc1},
{"TestFunc2", _test.TestFunc2},
}
var benchmarks = []testing.InternalBenchmark{
{"BenchmarkFunc1", _test.BenchmarkFunc1},
}
var examples = []testing.InternalExample{
{"ExampleFunc1", _test.ExampleFunc1, "", false},
}
func main() {
testdeps.ImportPath = "mypackage"
m := testing.MainStart(testdeps.TestDeps, tests, benchmarks, examples)
os.Exit(m.Run())
}
通过main方法,直到实际上是调用 testing.MainStart
获取了一个*testing.M
然后调用m.Run
这就是 Main 测试的执行原理。
test的main如何启动case
接下来看看testing.M
是什么
MainStart 初始化并生成了一个testing.M
Init操作是解析 go test
的命令行参数
testing.M
的结构如下
Go
type M struct {
deps testDeps
tests []InternalTest
benchmarks []InternalBenchmark
examples []InternalExample
timer *time.Timer
afterOnce sync.Once
numRun int
exitCode int
}
从上面的结构体可以看出,主要是三类测试用例:单元测试,性能测试和示例测试。
接下来看下Run方法:
首先根据命令参数,执行不同的逻辑:
*matchList
表示执行 go test -list regStr
表示不是真的执行测试,而是列出 regStr 匹配的case 列表:
在匹配的时候,会对三类用例的name都进行匹配
*shuffle
表示洗牌,也就是随机,使用随机包rand的Shuffle方法进行洗牌
接着执行befor,在befor里面,主要是对执行环境的一些初始化,或者对命令参数的设置等
在befor执行后,依次执行三类用例
等用例执行完成后,执行after,after是对执行结果的汇总等
最核心的就是三个方法:runTests,runExamples,runBenchmarks
单元测试 runTests
Go
func runTests(matchString func(pat, str string) (bool, error), tests []InternalTest, deadline time.Time) (ran, ok bool) {
ok = true
for _, procs := range cpuList {
runtime.GOMAXPROCS(procs)
for i := uint(0); i < *count; i++ {
if shouldFailFast() {
break
}
ctx := newTestContext(*parallel, newMatcher(matchString, *match, "-test.run"))
ctx.deadline = deadline
t := &T{
common: common{
signal: make(chan bool, 1),
barrier: make(chan bool),
w: os.Stdout,
},
context: ctx,
}
if Verbose() {
t.chatty = newChattyPrinter(t.w)
}
tRunner(t, func(t *T) {
for _, test := range tests {
t.Run(test.Name, test.F)
}
})
select {
case <-t.signal:
default:
panic("internal error: tRunner exited without sending on t.signal")
}
ok = ok && !t.Failed()
ran = ran || t.ran
}
}
return ran, ok
}
如果指定了cpu并且指定了count,那么会对单元测试执行 cpu数量乘以count次
接着初始化 TestContext
然后初始化testing.T
testing.T
组合了TestContext,并且组合了testing.common
testing.common
初始化了两个信号channel,用于控制单元测试执行。
最后调用tRunner
执行单元测试
tRunner
Go
func tRunner(t *T, fn func(t *T)) {
t.runner = callerName(0) // 获取当前测试函数的名称
//当这个goroutine完成时,要么是因为fn(t)
//正常返回或由于触发测试失败
//对运行时的调用。Goexit,记录持续时间并发送
//表示测试完成的信号。
defer func() {
// 测试失败,那么将失败数+1
if t.Failed() {
atomic.AddUint32(&numFailed, 1)
}
// 如果测试惊慌失措,请在终止之前打印任何测试输出。
err := recover()
signal := true
// 读锁定
t.mu.RLock()
// 获取完成状态
finished := t.finished
// 读锁定解锁
t.mu.RUnlock()
// 如果测试未完成,但是异常信息为空
if !finished && err == nil {
// 将错误信息赋值为空错误或空异常
err = errNilPanicOrGoexit
// 如果有父测试,当前是子测试
for p := t.parent; p != nil; p = p.parent {
p.mu.RLock()
finished = p.finished
p.mu.RUnlock()
if finished {
t.Errorf("%v: subtest may have called FailNow on a parent test", err)
err = nil
signal = false
break
}
}
}
// 使用延迟调用以确保我们报告测试
// 完成,即使清除函数调用t.FailNow。请参见第41355期。
didPanic := false
defer func() {
if didPanic {
return
}
if err != nil {
panic(err)
}
//只有在没有恐慌的情况下才报告测试完成,
//否则,测试二进制文件可以在死机之前退出
//报告给用户。请参见第41479期。
t.signal <- signal
}()
doPanic := func(err interface{}) {
// 设置测试失败
t.Fail()
if r := t.runCleanup(recoverAndReturnPanic); r != nil {
t.Logf("cleanup panicked with %v", r)
}
//在终止之前将输出日志刷新到根目录。
for root := &t.common; root.parent != nil; root = root.parent {
root.mu.Lock()
// 计算时间
root.duration += time.Since(root.start)
d := root.duration
root.mu.Unlock()
root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d))
if r := root.parent.runCleanup(recoverAndReturnPanic); r != nil {
fmt.Fprintf(root.parent.w, "cleanup panicked with %v", r)
}
}
didPanic = true
panic(err)
}
if err != nil {
doPanic(err)
}
t.duration += time.Since(t.start)
// 如果有子测试,当前是父测试
if len(t.sub) > 0 {
// 停止测试
t.context.release()
// 释放平行的子测验。
close(t.barrier)
// 等待子测验完成。
for _, sub := range t.sub {
<-sub.signal
}
cleanupStart := time.Now()
err := t.runCleanup(recoverAndReturnPanic)
t.duration += time.Since(cleanupStart)
if err != nil {
doPanic(err)
}
// 如果不是并发的
if !t.isParallel {
// 等待开始
t.context.waitParallel()
}
} else if t.isParallel { // 如果是并发的
//仅当此测试以并行方式运行时才释放其计数 测验请参阅Run方法中的注释。
t.context.release()
}
// 测试执行结束上报日志
t.report()
t.done = true
// 如果有父测试,那么设置执行标志
if t.parent != nil && atomic.LoadInt32(&t.hasSub) == 0 {
t.setRan()
}
}()
defer func() {
if len(t.sub) == 0 {
t.runCleanup(normalPanic)
}
}()
t.start = time.Now()
t.raceErrors = -race.Errors()
fn(t)
// code beyond here will not be executed when FailNow is invoked
t.mu.Lock()
t.finished = true
t.mu.Unlock()
}
在tRunner中执行的是 fn(t),其中t就是*testing.T
,这也是单元测试的写法标准:
Go
func TestXx(t *testing.T){}
而fn并不是我们在testing.M
中指定的单元测试键值对,而是在runTests
中进行二次包装的
换句话说,我们自己写的单元测试,被测试框架经过模板生成test的main启动,然后在进行了初始化后,
进行了按照参数进行分批,接着在goroutine中,按照分配的case进行逐个执行。
testing.T.Run
Go
// 将运行f作为名为name的t的子测试。它在一个单独的goroutine中运行f
// 并且阻塞直到f返回或调用t。并行成为并行测试。
// 运行报告f是否成功(或者至少在调用t.Parallel之前没有失败)。
//
// Run可以从多个goroutine同时调用,但所有此类调用
// 必须在t的外部测试函数返回之前返回。
func (t *T) Run(name string, f func(t *T)) bool {
// 将子测试的数量+1
atomic.StoreInt32(&t.hasSub, 1)
// 获取匹配的测试name
testName, ok, _ := t.context.match.fullName(&t.common, name)
// 如果没有配置,那么直接结束
if !ok || shouldFailFast() {
return true
}
//记录此调用点的堆栈跟踪,以便如果子测试
//在单独的堆栈中运行的函数被标记为助手,我们可以
//继续将堆栈遍历到父测试中。
var pc [maxStackLen]uintptr
// 获取调用者的函数name
n := runtime.Callers(2, pc[:])
t = &T{ // 创建一个新的 testing.T 用于执行子测试
common: common{
barrier: make(chan bool),
signal: make(chan bool, 1),
name: testName,
parent: &t.common,
level: t.level + 1,
creator: pc[:n],
chatty: t.chatty,
},
context: t.context,
}
t.w = indenter{&t.common}
if t.chatty != nil {
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
}
//而不是在调用之前减少此测试的运行计数
//tRunner并在之后增加它,我们依靠tRunner保持
//计数正确。这样可以确保运行一系列顺序测试
//而不会被抢占,即使它们的父级是并行测试。这
//如果*parallel==1,则可以特别减少意外。
go tRunner(t, f)
if !<-t.signal {
//此时,FailNow很可能是在
//其中一个子测验的家长测验。继续中止链的上行。
runtime.Goexit()
}
return !t.failed
}
示例测试 runExamples
Go
func runExamples(matchString func(pat, str string) (bool, error), examples []InternalExample) (ran, ok bool) {
ok = true
var eg InternalExample
for _, eg = range examples {
matched, err := matchString(*match, eg.Name)
if err != nil {
fmt.Fprintf(os.Stderr, "testing: invalid regexp for -test.run: %s\n", err)
os.Exit(1)
}
if !matched {
continue
}
ran = true
if !runExample(eg) {
ok = false
}
}
return ran, ok
}
示例测试就简单一点了,首先根据正则进行匹配,匹配到了就执行,否则就跳过,出错就退出
runExample
在runExample中,首先对标准输出进行拷贝,将控制输出进行解析
然后在defer中对输出进行比对
processRunResult
输出结果比对就简单,主要是字符串的一些比较
在示例测试中,输出结果的行不需要顺序一致,是因为在比对前,会进行排序
性能测试 runBenchmarks
性能测试和单元测试差不多,只是结构体不同,性能测试的结构体是testing.B
同样的,也是先创建了一个main的testing.B用于启动性能测试,相当于作为初始case
然后启动初始case的runN启动
runN
runN作为启动性能测试的初始测试,也是逐个执行用户定义的性能测试case
实际执行的是testing.B.Run
方法
testing.B.Run
testing.B.Run
与testing.T.Run
类似,主要是对子测试等做处理,然后执行用户的case