当前位置:   article > 正文

Go-知识测试-工作机制

Go-知识测试-工作机制


在 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 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())
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

通过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       testDeps
	tests      []InternalTest
	benchmarks []InternalBenchmark
	examples   []InternalExample
	timer     *time.Timer
	afterOnce sync.Once
	numRun int
	exitCode int
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

从上面的结构体可以看出,主要是三类测试用例:单元测试,性能测试和示例测试。
接下来看下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 = 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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

如果指定了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() {
		// 测试失败,那么将失败数+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()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127

在tRunner中执行的是 fn(t),其中t就是*testing.T,这也是单元测试的写法标准:

func TestXx(t *testing.T){}
  • 1

而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 {
	// 将子测试的数量+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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

示例测试 runExamples

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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

示例测试就简单一点了,首先根据正则进行匹配,匹配到了就执行,否则就跳过,出错就退出

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
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/790163
推荐阅读
相关标签
  

闽ICP备14008679号