赞
踩
在 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 函数来链接这个包和测试框架,生成测试的二进制文件。
详细的来说:
首先执行 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 中,渲染后的代码如下
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 测试的执行原理。
接下来看看testing.M
是什么
MainStart 初始化并生成了一个testing.M
Init操作是解析 go test
的命令行参数
testing.M
的结构如下
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
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
执行单元测试
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
,这也是单元测试的写法标准:
func TestXx(t *testing.T){}
而fn并不是我们在testing.M
中指定的单元测试键值对,而是在runTests
中进行二次包装的
换句话说,我们自己写的单元测试,被测试框架经过模板生成test的main启动,然后在进行了初始化后,
进行了按照参数进行分批,接着在goroutine中,按照分配的case进行逐个执行。
// 将运行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 }
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中,首先对标准输出进行拷贝,将控制输出进行解析
然后在defer中对输出进行比对
输出结果比对就简单,主要是字符串的一些比较
在示例测试中,输出结果的行不需要顺序一致,是因为在比对前,会进行排序
性能测试和单元测试差不多,只是结构体不同,性能测试的结构体是testing.B
同样的,也是先创建了一个main的testing.B用于启动性能测试,相当于作为初始case
然后启动初始case的runN启动
runN作为启动性能测试的初始测试,也是逐个执行用户定义的性能测试case
实际执行的是testing.B.Run
方法
testing.B.Run
与testing.T.Run
类似,主要是对子测试等做处理,然后执行用户的case
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。