欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > IT业 > Go-知识测试-工作机制

Go-知识测试-工作机制

2024/10/25 13:26:31 来源:https://blog.csdn.net/a18792721831/article/details/140062769  浏览:    关键词:Go-知识测试-工作机制

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 命令的大致执行流程:

  1. 首先,go test 命令会解析命令行参数,获取需要测试的包和测试选项。
  2. 然后,go test 命令会构建一个测试的二进制文件。这个二进制文件包含了需要测试的包和测试用例,以及测试用例的运行环境和测试框架。
  3. 接着,go test 命令会启动这个二进制文件,并将命令行参数传递给它。这个二进制文件会运行测试用例,并将测试结果输出到标准输出。
  4. 最后,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.Runtesting.T.Run类似,主要是对子测试等做处理,然后执行用户的case
在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com