Go-知識測試-工作機制
- 生成test的main
- test的main如何啟動case
- 單元測試 runTests
- tRunner
- testing.T.Run
- 示例測試 runExamples
- runExample
- processRunResult
- 性能測試 runBenchmarks
- runN
- testing.B.Run
在 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 中,渲染后的代碼如下
package mainimport ("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
的結構如下
type M struct {deps testDepstests []InternalTestbenchmarks []InternalBenchmarkexamples []InternalExampletimer *time.TimerafterOnce sync.OncenumRun intexitCode int
}
從上面的結構體可以看出,主要是三類測試用例:單元測試,性能測試和示例測試。
接下來看下Run方法:
首先根據命令參數,執行不同的邏輯:
*matchList
表示執行 go test -list regStr
表示不是真的執行測試,而是列出 regStr 匹配的case 列表:
在匹配的時候,會對三類用例的name都進行匹配
*shuffle
表示洗牌,也就是隨機,使用隨機包rand的Shuffle方法進行洗牌
接著執行befor,在befor里面,主要是對執行環境的一些初始化,或者對命令參數的設置等
在befor執行后,依次執行三類用例
等用例執行完成后,執行after,after是對執行結果的匯總等
最核心的就是三個方法:runTests,runExamples,runBenchmarks
單元測試 runTests
func runTests(matchString func(pat, str string) (bool, error), tests []InternalTest, deadline time.Time) (ran, ok bool) {ok = truefor _, 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 = deadlinet := &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
func tRunner(t *T, fn func(t *T)) {t.runner = callerName(0) // 獲取當前測試函數的名稱//當這個goroutine完成時,要么是因為fn(t)//正常返回或由于觸發測試失敗//對運行時的調用。Goexit,記錄持續時間并發送//表示測試完成的信號。defer func() {// 測試失敗,那么將失敗數+1if 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.finishedp.mu.RUnlock()if finished {t.Errorf("%v: subtest may have called FailNow on a parent test", err)err = nilsignal = falsebreak}}}// 使用延遲調用以確保我們報告測試// 完成,即使清除函數調用t.FailNow。請參見第41355期。didPanic := falsedefer 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.durationroot.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 = truepanic(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 invokedt.mu.Lock()t.finished = truet.mu.Unlock()
}
在tRunner中執行的是 fn(t),其中t就是*testing.T
,這也是單元測試的寫法標準:
func TestXx(t *testing.T){}
而fn并不是我們在testing.M
中指定的單元測試鍵值對,而是在runTests
中進行二次包裝的
換句話說,我們自己寫的單元測試,被測試框架經過模板生成test的main啟動,然后在進行了初始化后,
進行了按照參數進行分批,接著在goroutine中,按照分配的case進行逐個執行。
testing.T.Run
// 將運行f作為名為name的t的子測試。它在一個單獨的goroutine中運行f
// 并且阻塞直到f返回或調用t。并行成為并行測試。
// 運行報告f是否成功(或者至少在調用t.Parallel之前沒有失敗)。
//
// Run可以從多個goroutine同時調用,但所有此類調用
// 必須在t的外部測試函數返回之前返回。
func (t *T) Run(name string, f func(t *T)) bool {// 將子測試的數量+1atomic.StoreInt32(&t.hasSub, 1)// 獲取匹配的測試nametestName, ok, _ := t.context.match.fullName(&t.common, name)// 如果沒有配置,那么直接結束if !ok || shouldFailFast() {return true}//記錄此調用點的堆棧跟蹤,以便如果子測試//在單獨的堆棧中運行的函數被標記為助手,我們可以//繼續將堆棧遍歷到父測試中。var pc [maxStackLen]uintptr// 獲取調用者的函數namen := 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
func runExamples(matchString func(pat, str string) (bool, error), examples []InternalExample) (ran, ok bool) {ok = truevar eg InternalExamplefor _, 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 = trueif !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