*Main测试.即声明一个func TestMain(m testing.M).它是名字比较特殊的测试.
参数类型为testing.M指针.如果声明了这样一个函数.那么当前测试程序不是直接执
行各项测试.而是将测试交给TestMain调度.
示例:
package goTest
import (
"os"
"testing"
)
func ExampleSayHello() {
SayHello()
//Output: Hello World
}
func ExampleSayGoodBye() {
SayGoodBye()
//Output:
//Hello World
//go
}
func ExamplePrintNames() {
PrintNames()
//Unordered output:
//1
//2
//3
//4
}
func TestMain(m *testing.M) {
println("TestMain setup.")
retCode := m.Run()
println("TestMain teardown.")
os.Exit(retCode)
}
执行结果:
编辑
如果所有测试均通过测试.m.Run()返回0.如果m.Run()返回1.则代表测试失败.
1.实现原理:
每个测试函数都需要一个能够控制测试流程及标记测试的参数.
单元测试需要testing.T.
性能测试需要testing.B.
模糊测试需要testing.F.
这几个参数类型都共享同一个基础类型.那就是testing.common.所以这几个测试类都有类似行为.
1.1:数据结构:
源码位置:src/testing/testing.go
每一个测试对应一个testing.common.
type common struct {
mu sync.RWMutex // guards this group of fields
output []byte // Output generated by test or benchmark.
w io.Writer // For flushToParent.
ran bool // Test or benchmark (or one of its subtests) was executed.
failed bool // Test or benchmark has failed.
skipped bool // Test or benchmark has been skipped.
done bool // Test is finished and all subtests have completed.
helperPCs map[uintptr]struct{} // functions to be skipped when writing file/line info
helperNames map[string]struct{} // helperPCs converted to function names
cleanups []func() // optional functions to be called at the end of the test
cleanupName string // Name of the cleanup function.
cleanupPc []uintptr // The stack trace at the point where Cleanup was called.
finished bool // Test function has completed.
inFuzzFn bool // Whether the fuzz target, if this is one, is running.
chatty *chattyPrinter // A copy of chattyPrinter, if the chatty flag is set.
bench bool // Whether the current test is a benchmark.
hasSub atomic.Bool // whether there are sub-benchmarks.
cleanupStarted atomic.Bool // Registered cleanup callbacks have started to execute
runner string // Function name of tRunner running the test.
isParallel bool // Whether the test is parallel.
parent *common
level int // Nesting depth of test or benchmark.
creator []uintptr // If level > 0, the stack trace at the point where the parent called t.Run.
name string // Name of test or benchmark.
start highPrecisionTime // Time test or benchmark started
duration time.Duration
barrier chan bool // To signal parallel subtests they may start. Nil when T.Parallel is not present (B) or not usable (when fuzzing).
signal chan bool // To signal a test is done.
sub []*T // Queue of subtests to be run in parallel.
lastRaceErrors atomic.Int64 // Max value of race.Errors seen during the test or its subtests.
raceErrorLogged atomic.Bool
tempDirMu sync.Mutex
tempDir string
tempDirErr error
tempDirSeq int32
ctx context.Context
cancelCtx context.CancelFunc
}
1).核心锁与状态:
mu:读写锁.保证下面并发状态字段安全.
ran:执行标记.标记测试 基准测试 或其子测试是否已执行.避免重复执行.
failed:失败标记.测试是否失败.调用t.Error或t.Fatal会置为true.
skipped:测试是否已被跳过.调用t.skip会置为true.
done:完成标记.测试(含所有子测试)是否完全结束.
finished:函数完成标记.仅标记测试函数本身是否执行完毕.(区别于done.done包含子测试).
inFuzzFn:模糊测试标记.仅模糊测试场景.标记模糊函数目标是否正在运行.
2).日志与输出:
output:输出缓冲区.存储测试过程中.t.Log/t.Error等产生的日志.(批量刷出.减少io次数).
w:输出写入器.用于将output刷到父测试/控制台.(比如flushToParent方法会用这个writer).
chatty:详细输出器.仅当-v(详细模式)时生效.控制日志的详细格式.比如显示文件或行号.
3).辅助函数与栈追踪:
helperPCs: 辅助函数 PC 集合. 存储「需要跳过的辅助函数地址」(调用 t.Helper() 时会把当前函数加入),日志中不显示这些函数的栈帧,让报错行
更精准 .
helperNames: 辅助函数名集合 helperPCs转换后的函数名(避免重复解析 PC
地址).
creator:子测试创建栈.当有子测试时.存储父测试调用t.run时的栈追踪信息.用于调
用定位子测试的位置.
cleanupPc:清理函数栈.存储注册栈函数的栈信息.清理执行函数失败时.用于定位报
错位置.
4).清理函数(资源释放):
cleanups:清理函数列表.存储调用t.Cleapup(f)注册的清理函数.测试结束时.逆序执
行.后进先出.
cleapupName:清理函数名.记录当前执行的清理函数的名称.用于日志 报错.
cleapupStarted:清理启动标记.
5).基准测试:
bench:基准测试标记.
hasSub:子基准测试标记.
6).父子测试与层级:
parent:父测试指针.
level:嵌套层级.
name:测试名称.
runner:运行函数名.
7).计时与耗时:
start:启动时间.
duration:执行耗时.
8).并发控制:
isParallel:并行标记.
barrier:并行屏障.
signal:完成信号.
sub:并行子测试队列.
9).静态检测:
lastRaceErrors:竞态错误数.
raceErrorLogged:竟态日志标记.
10).临时目录:
tempDirmu:临时目录锁.
tempDir:临时目录路径.
tempDirErr:临时目录错误.
tempDirSeq:临时目录序号.
11).上下文:
ctx:测试上下文.
cancelCtx:上下文取消函数.
通过数据结构可以看出testing.common不仅记录了测试函数的基础信息.比如名字.
还管理了测试执行过程的测试结果.可以说testing.common是单元测试 性能测试和
模糊测试的基石.通过继承共同的结构.保证了各种测试的一致性.降低了使用门槛.
2.testing.TB接口:
testing.common实现的接口为testing.TB.单元测试和性能测试正是通过该接口获
取基础能力.
2.1接口定义:
源码位置:src/testing/testing.go:
// TB is the interface common to T, B, and F.
type TB interface {
Cleanup(func())
Error(args ...any)
Errorf(format string, args ...any)
Fail()
FailNow()
Failed() bool
Fatal(args ...any)
Fatalf(format string, args ...any)
Helper()
Log(args ...any)
Logf(format string, args ...any)
Name() string
Setenv(key, value string)
Chdir(dir string)
Skip(args ...any)
SkipNow()
Skipf(format string, args ...any)
Skipped() bool
TempDir() string
Context() context.Context
// A private method to prevent users implementing the
// interface and so future additions to it will not
// violate Go 1 compatibility.
private()
}
1).测试状态标记:
Fail():标记测试失败.仅标记failed=true.不终止测试.后续代码继续执行.
FailNow():标记失败并立即终止. 直接终止当前测试协程(调
用 runtime.Goexit() ),子测试也会终止;B/F中也适用,基准测试 / 模糊测试
会立即停止 .
Failed() bool:检查是否失败.返回当前测试的失败状态.
2).错误/终止类:
Error(args ...any):记录错误.并标记失败.
Errorf(format string,args ...any):格式化记录错误并标记失败.
Fatal(args ...any):记录错误 标记失败并终止.
Fatalf(format string,args ...any):格式化记录错误.并标记失败终止.
3).跳过测试类:
Skip(args ...any):记录跳过信息并标记跳过.
Skipf(format string,args ...any):格式化记录跳过信息并标记跳过.
SkipNow():立即跳过测试.
Skipped() bool:检查是否跳过.
4).日志类:
Log(args ...any):记录普通日志.
Logf(format string,args ...any):格式化记录普通日志.
5).辅助工具类:
Helper():标记当前函数为辅助函数.
Name() string:获取测试名称.
TempDir() string:创建临时目录.
Setenv(key,value string):设置环境变量.
Chdir(dir string):切换工作目录.
6).资源/生命周期管控:
Cleanup(func()):注册清理函数.
Context() context.Context:获取测试上下文.
7).私有方法:
private():私有占位方法.无实际逻辑.仅用于阻止用户自定义实现TB接口.
3.单元测试实现原理:
3.1数据结构:
源码位置:
src/testing/testing.go:T
type T struct {
common
denyParallel bool
tstate *testState // For running tests and subtests.
}
common:前面介绍的testing.common.
isParallel:表示当前测试是否需要并发.如果测试中执行了t.Parallel().则此值为
true.
tstate:测试状态管理器.
3.2测试执行:tRunner():
源码位置:src/testing/testing.go
func tRunner(t *T, fn func(t *T)) {
t.runner = callerName(0)
defer func() {
t.checkRaces()
if t.Failed() {
numFailed.Add(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 {
if !t.isParallel {
t.Errorf("%v: subtest may have called FailNow on a parent test", err)
err = nil
}
signal = false
break
}
}
}
if err != nil && t.tstate.isFuzzing {
prefix := "panic: "
if err == errNilPanicOrGoexit {
prefix = ""
}
t.Errorf("%s%s\n%s\n", prefix, err, string(debug.Stack()))
t.mu.Lock()
t.finished = true
t.mu.Unlock()
err = nil
}
didPanic := false
defer func() {
// Only report that the test is complete if it doesn't panic,
// as otherwise the test binary can exit before the panic is
// reported to the user. See issue 41479.
if didPanic {
return
}
if err != nil {
panic(err)
}
running.Delete(t.name)
t.signal <- signal
}()
doPanic := func(err any) {
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 += highPrecisionTimeSince(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 += highPrecisionTimeSince(t.start)
if len(t.sub) > 0 {
t.tstate.release()
running.Delete(t.name)
close(t.barrier)
// Wait for subtests to complete.
for _, sub := range t.sub {
<-sub.signal
}
cleanupStart := highPrecisionTimeNow()
running.Store(t.name, cleanupStart)
err := t.runCleanup(recoverAndReturnPanic)
t.duration += highPrecisionTimeSince(cleanupStart)
if err != nil {
doPanic(err)
}
t.checkRaces()
if !t.isParallel {
t.tstate.waitParallel()
}
} else if t.isParallel {
t.tstate.release()
}
t.report() // Report after all subtests have finished.
t.done = true
if t.parent != nil && !t.hasSub.Load() {
t.setRan()
}
}()
defer func() {
if len(t.sub) == 0 {
t.runCleanup(normalPanic)
}
}()
t.start = highPrecisionTimeNow()
t.resetRaces()
fn(t)
// code beyond here will not be executed when FailNow is invoked
t.mu.Lock()
t.finished = true
t.mu.Unlock()
}
1).初始化函数:
t.runner:=callerName(0)
记录当前tRunner协程对应的调用者函数名.用于日志和调试.
callerName(0)解析调用栈获取测试函数的名称.
2).核心defer收尾:
所有测试执行后的收尾操作都在这里.包括.统计失败数.处理panic/Goexit 执行清理函数 协调并行子测试 完成发送信号.
模糊测试panic处理:
最终panic处理与完成信号发送.
并行子测试处理:
3).清理函数的保底执行:
normalPanic:表示函数执行清理时.若panic则正常抛出.区别于recoverAndReturnPanic捕获并返回.
4).执行测试函数:
如果在测试函数中调用t.FailNow()(底层是runtime.Goexit).则fn(t)之后的代码不会在执行.finished仍标记位false.
流程图:
3.3启动子测试Run():
源码位置:src/testing/testing.go
func (t *T) Run(name string, f func(t *T)) bool {
if t.cleanupStarted.Load() {
panic("testing: t.Run called during t.Cleanup")
}
t.hasSub.Store(true)
testName, ok, _ := t.tstate.match.fullName(&t.common, name)
if !ok || shouldFailFast() {
return true
}
var pc [maxStackLen]uintptr
n := runtime.Callers(2, pc[:])
ctx, cancelCtx := context.WithCancel(context.Background())
t = &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,
ctx: ctx,
cancelCtx: cancelCtx,
},
tstate: t.tstate,
}
t.w = indenter{&t.common}
if t.chatty != nil {
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
}
running.Store(t.name, highPrecisionTimeNow())
go tRunner(t, f)
if !<-t.signal {
// At this point, it is likely that FailNow was called on one of the
// parent tests by one of the subtests. Continue aborting up the chain.
runtime.Goexit()
}
if t.chatty != nil && t.chatty.json {
t.chatty.Updatef(t.parent.name, "=== NAME %s\n", t.parent.name)
}
return !t.failed
}
1).前置校验.(禁止清理阶段调用):
2).子测试元信息初始化:
3).子测试实例创建:
4)子测试启动与日志输出:
5).等待子测试完成信号.
6).收尾返回结果:
流程图:
3.4并发测试Parallel():
源码位置:src/testing/testing.go
func (t *T) Parallel() {
if t.isParallel {
panic("testing: t.Parallel called multiple times")
}
if t.denyParallel {
panic(parallelConflict)
}
t.isParallel = true
if t.parent.barrier == nil {
return
}
t.duration += highPrecisionTimeSince(t.start)
// Add to the list of tests to be released by the parent.
t.parent.sub = append(t.parent.sub, t)
t.checkRaces()
if t.chatty != nil {
t.chatty.Updatef(t.name, "=== PAUSE %s\n", t.name)
}
running.Delete(t.name)
t.signal <- true // Release calling test.
<-t.parent.barrier // Wait for the parent test to complete.
t.tstate.waitParallel()
if t.chatty != nil {
t.chatty.Updatef(t.name, "=== CONT %s\n", t.name)
}
running.Store(t.name, highPrecisionTimeNow())
t.start = highPrecisionTimeNow()
t.lastRaceErrors.Store(int64(race.Errors()))
}
1). 前置校验(禁止重复 / 非法调用):
2). 标记为并行测试:
3). 耗时统计修正(排除等待时间):
累加当前执行的时间.避免计算到等待时间.
4). 加入父测试的并行队列:
5). 竞态检测清理(避免误报):
6). 输出暂停日志并标记为非运行状态:
7). 释放父测试并等待并行屏障:
8). 获取并行计数并恢复执行:
流程图:
语雀地址www.yuque.com/itbosunmian… 《Go.》 密码:xbkk 欢迎大家访问.提意见.
王维诗里的红豆.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路