每种测试(单元测试 性能测试或示例测试)都有一个数据类型与其对应.
单元测试:InternalTest.
性能测试:InternalBenchmark.
示例测试:InternalExample.
在测试编译阶段.每个测试都会被放到指定类型的切片中.测试执行时.这些测试会被放
到testing.M数据结构中进行调度.testing.M是MainTest对应的数据结构.
1.数据结构:
源码位置:src/testing/testing.go:M
type M struct {
deps testDeps
tests []InternalTest
benchmarks []InternalBenchmark
fuzzTargets []InternalFuzzTarget
examples []InternalExample
timer *time.Timer
afterOnce sync.Once
numRun int
// value to pass to os.Exit, the outer test func main
// harness calls os.Exit with this code. See #34129.
exitCode int
}
deps:测试依赖接口.封装测试需要的底层依赖.如命令行参数 获取测试二进制文件路
径等.
tests:单元测试切片.
benchmarks:基准测试切片.
fuzzTargets:模糊测试切片.
examples:示例测试切片.
timer:全局测试超时定时器.
afterOnce:保证超时逻辑只执行一次.
numRun:已执行的测试数量.
exitCode:测试进程退出码.
2.生命周期:
3.执行测试:
源码位置:src/testing/testing.go
func (m *M) Run() (code int) {
defer func() {
code = m.exitCode
}()
m.numRun++
// TestMain may have already called flag.Parse.
if !flag.Parsed() {
flag.Parse()
}
if chatty.json {
realStderr = os.Stderr
os.Stderr = os.Stdout
}
if *parallel < 1 {
fmt.Fprintln(os.Stderr, "testing: -parallel can only be given a positive integer")
flag.Usage()
m.exitCode = 2
return
}
if *matchFuzz != "" && *fuzzCacheDir == "" {
fmt.Fprintln(os.Stderr, "testing: -test.fuzzcachedir must be set if -test.fuzz is set")
flag.Usage()
m.exitCode = 2
return
}
if *matchList != "" {
listTests(m.deps.MatchString, m.tests, m.benchmarks, m.fuzzTargets, m.examples)
m.exitCode = 0
return
}
if *shuffle != "off" {
var n int64
var err error
if *shuffle == "on" {
n = time.Now().UnixNano()
} else {
n, err = strconv.ParseInt(*shuffle, 10, 64)
if err != nil {
fmt.Fprintln(os.Stderr, `testing: -shuffle should be "off", "on", or a valid integer:`, err)
m.exitCode = 2
return
}
}
fmt.Println("-test.shuffle", n)
rng := rand.New(rand.NewSource(n))
rng.Shuffle(len(m.tests), func(i, j int) { m.tests[i], m.tests[j] = m.tests[j], m.tests[i] })
rng.Shuffle(len(m.benchmarks), func(i, j int) { m.benchmarks[i], m.benchmarks[j] = m.benchmarks[j], m.benchmarks[i] })
}
parseCpuList()
m.before()
defer m.after()
if !*isFuzzWorker {
deadline := m.startAlarm()
haveExamples = len(m.examples) > 0
testRan, testOk := runTests(m.deps.MatchString, m.tests, deadline)
fuzzTargetsRan, fuzzTargetsOk := runFuzzTests(m.deps, m.fuzzTargets, deadline)
exampleRan, exampleOk := runExamples(m.deps.MatchString, m.examples)
m.stopAlarm()
if !testRan && !exampleRan && !fuzzTargetsRan && *matchBenchmarks == "" && *matchFuzz == "" {
fmt.Fprintln(os.Stderr, "testing: warning: no tests to run")
if testingTesting && *match != "^$" {
fmt.Print(chatty.prefix(), "FAIL: package testing must run tests\n")
testOk = false
}
}
anyFailed := !testOk || !exampleOk || !fuzzTargetsOk || !runBenchmarks(m.deps.ImportPath(), m.deps.MatchString, m.benchmarks)
if !anyFailed && race.Errors() > 0 {
fmt.Print(chatty.prefix(), "testing: race detected outside of test execution\n")
anyFailed = true
}
if anyFailed {
fmt.Print(chatty.prefix(), "FAIL\n")
m.exitCode = 1
return
}
}
fuzzingOk := runFuzzing(m.deps, m.fuzzTargets)
if !fuzzingOk {
fmt.Print(chatty.prefix(), "FAIL\n")
if *isFuzzWorker {
m.exitCode = fuzzWorkerExitCode
} else {
m.exitCode = 1
}
return
}
m.exitCode = 0
if !*isFuzzWorker {
fmt.Print(chatty.prefix(), "PASS\n")
}
return
}
该方法会执行单元测试 性能测试和示例测试.如果实现了TestMain()但没有调用
m.Run().什么测试都不会执行.
m.Run()不仅会执行测试.还会做一些初始化工作.比如解析参数 启动定时器 根据参
数指示创建一系列的文件等.
1).退出码(defer初始化):
无论方法是否正常返回还是panic.最终都会将m.exitCode赋值给返回code.保证返
回码与M的状态一致,
2).统计run调用次数:
兼容历史问题.
3).解析命令行参数(未解析):
确保命令行参数.如-v -run -timeout已被解析.避免后续逻辑使用未解析的参数.
4).json模式下统一stdout/stdeer(解决输出乱序):
仅解决 Go 代码的输出乱序,子进程 / 运行时(如 println )的输出仍可能乱序.
5).参数合法性校验:
6).处理-list参数.(仅列出测试用例.不执行):
当传入 -list 参数时,仅打印匹配的测试 / 基准 / 示例名称,不执行任何测试,
*直接返回成功( *exitCode=0 ).
7).处理-shuffile参数.(随机打乱测试执行顺序):
随机打乱测试执行顺序,暴露因测试顺序依赖导致的隐藏问题.
8).初始化CPU列表:
解析-cpu参数.
9).执行前置和后置钩子:
10).执行非模糊测试工作进程:
11).执行模糊测试:
12).执行成功.设置退出码:
流程图:
4.go test工作机制:
Go有多个命令行工具.go test只是其中一个.go test 命令的函数入口在
src/cmd/go/internal/test/test.go:runTest().
func runTest(ctx context.Context, cmd *base.Command, args []string) {
pkgArgs, testArgs = testFlags(args)
modload.InitWorkfile() // The test command does custom flag processing; initialize workspaces after that.
if cfg.DebugTrace != "" {
var close func() error
var err error
ctx, close, err = trace.Start(ctx, cfg.DebugTrace)
if err != nil {
base.Fatalf("failed to start trace: %v", err)
}
defer func() {
if err := close(); err != nil {
base.Fatalf("failed to stop trace: %v", err)
}
}()
}
ctx, span := trace.StartSpan(ctx, fmt.Sprint("Running ", cmd.Name(), " command"))
defer span.Done()
work.FindExecCmd() // initialize cached result
work.BuildInit()
work.VetFlags = testVet.flags
work.VetExplicit = testVet.explicit
pkgOpts := load.PackageOpts{ModResolveTests: true}
pkgs = load.PackagesAndErrors(ctx, pkgOpts, pkgArgs)
if len(pkgs) == 0 {
base.Fatalf("no packages to test")
}
if testFuzz != "" {
if !platform.FuzzSupported(cfg.Goos, cfg.Goarch) {
base.Fatalf("-fuzz flag is not supported on %s/%s", cfg.Goos, cfg.Goarch)
}
if len(pkgs) != 1 {
base.Fatalf("cannot use -fuzz flag with multiple packages")
}
if testCoverProfile != "" {
base.Fatalf("cannot use -coverprofile flag with -fuzz flag")
}
if profileFlag := testProfile(); profileFlag != "" {
base.Fatalf("cannot use %s flag with -fuzz flag", profileFlag)
}
mainMods := modload.MainModules
if m := pkgs[0].Module; m != nil && m.Path != "" {
if !mainMods.Contains(m.Path) {
base.Fatalf("cannot use -fuzz flag on package outside the main module")
}
} else if pkgs[0].Standard && modload.Enabled() {
if strings.HasPrefix(pkgs[0].ImportPath, "cmd/") {
if !mainMods.Contains("cmd") || !mainMods.InGorootSrc(module.Version{Path: "cmd"}) {
base.Fatalf("cannot use -fuzz flag on package outside the main module")
}
} else {
if !mainMods.Contains("std") || !mainMods.InGorootSrc(module.Version{Path: "std"}) {
base.Fatalf("cannot use -fuzz flag on package outside the main module")
}
}
}
}
if testProfile() != "" && len(pkgs) != 1 {
base.Fatalf("cannot use %s flag with multiple packages", testProfile())
}
if testO != "" {
if strings.HasSuffix(testO, "/") || strings.HasSuffix(testO, string(os.PathSeparator)) {
testODir = true
} else if fi, err := os.Stat(testO); err == nil && fi.IsDir() {
testODir = true
}
}
if len(pkgs) > 1 && (testC || testO != "") && !base.IsNull(testO) {
if testO != "" && !testODir {
base.Fatalf("with multiple packages, -o must refer to a directory or %s", os.DevNull)
}
pkgsForBinary := map[string][]*load.Package{}
for _, p := range pkgs {
testBinary := testBinaryName(p)
pkgsForBinary[testBinary] = append(pkgsForBinary[testBinary], p)
}
for testBinary, pkgs := range pkgsForBinary {
if len(pkgs) > 1 {
var buf strings.Builder
for _, pkg := range pkgs {
buf.WriteString(pkg.ImportPath)
buf.WriteString("\n")
}
base.Errorf("cannot write test binary %s for multiple packages:\n%s", testBinary, buf.String())
}
}
base.ExitIfErrors()
}
initCoverProfile()
defer closeCoverProfile()
if testTimeout > 0 && testFuzz == "" {
if wd := testTimeout / 10; wd < 5*time.Second {
testWaitDelay = 5 * time.Second
} else {
testWaitDelay = wd
}
if testWaitDelay < 1*time.Minute {
testKillTimeout = testTimeout + 1*time.Minute
} else {
testKillTimeout = testTimeout + testWaitDelay
}
}
if dir, _ := cache.DefaultDir(); dir != "off" {
if data, _ := lockedfile.Read(filepath.Join(dir, "testexpire.txt")); len(data) > 0 && data[len(data)-1] == '\n' {
if t, err := strconv.ParseInt(string(data[:len(data)-1]), 10, 64); err == nil {
testCacheExpire = time.Unix(0, t)
}
}
}
b := work.NewBuilder("")
defer func() {
if err := b.Close(); err != nil {
base.Fatal(err)
}
}()
var builds, runs, prints []*work.Action
var writeCoverMetaAct *work.Action
if cfg.BuildCoverPkg != nil {
match := make([]func(*load.Package) bool, len(cfg.BuildCoverPkg))
for i := range cfg.BuildCoverPkg {
match[i] = load.MatchPackage(cfg.BuildCoverPkg[i], base.Cwd())
}
// Select for coverage all dependencies matching the -coverpkg
// patterns.
plist := load.TestPackageList(ctx, pkgOpts, pkgs)
testCoverPkgs = load.SelectCoverPackages(plist, match, "test")
if cfg.Experiment.CoverageRedesign && len(testCoverPkgs) > 0 {
writeCoverMetaAct = &work.Action{
Mode: "write coverage meta-data file",
Actor: work.ActorFunc(work.WriteCoverMetaFilesFile),
Objdir: b.NewObjdir(),
}
for _, p := range testCoverPkgs {
p.Internal.Cover.GenMeta = true
}
}
}
// Inform the compiler that it should instrument the binary at
// build-time when fuzzing is enabled.
if testFuzz != "" {
// Don't instrument packages which may affect coverage guidance but are
// unlikely to be useful. Most of these are used by the testing or
// internal/fuzz packages concurrently with fuzzing.
var skipInstrumentation = map[string]bool{
"context": true,
"internal/fuzz": true,
"internal/godebug": true,
"internal/runtime/maps": true,
"internal/sync": true,
"reflect": true,
"runtime": true,
"sync": true,
"sync/atomic": true,
"syscall": true,
"testing": true,
"time": true,
}
for _, p := range load.TestPackageList(ctx, pkgOpts, pkgs) {
if !skipInstrumentation[p.ImportPath] {
p.Internal.FuzzInstrument = true
}
}
}
// Collect all the packages imported by the packages being tested.
allImports := make(map[*load.Package]bool)
for _, p := range pkgs {
if p.Error != nil && p.Error.IsImportCycle {
continue
}
for _, p1 := range p.Internal.Imports {
allImports[p1] = true
}
}
if cfg.BuildCover {
for _, p := range pkgs {
if cfg.BuildCoverMode == "atomic" && p.ImportPath != "sync/atomic" {
load.EnsureImport(p, "sync/atomic")
}
if len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 &&
cfg.BuildCoverPkg == nil &&
cfg.Experiment.CoverageRedesign {
p.Internal.Cover.GenMeta = true
}
}
}
// Prepare build + run + print actions for all packages being tested.
for _, p := range pkgs {
reportErr := func(perr *load.Package, err error) {
str := err.Error()
if p.ImportPath != "" {
load.DefaultPrinter().Errorf(perr, "# %s\n%s", p.ImportPath, str)
} else {
load.DefaultPrinter().Errorf(perr, "%s", str)
}
}
reportSetupFailed := func(perr *load.Package, err error) {
var stdout io.Writer = os.Stdout
if testJSON {
json := test2json.NewConverter(stdout, p.ImportPath, test2json.Timestamp)
defer func() {
json.Exited(err)
json.Close()
}()
if gotestjsonbuildtext.Value() == "1" {
// While this flag is about go build -json, the other effect
// of that change was to include "FailedBuild" in the test JSON.
gotestjsonbuildtext.IncNonDefault()
} else {
json.SetFailedBuild(perr.Desc())
}
stdout = json
}
fmt.Fprintf(stdout, "FAIL\t%s [setup failed]\n", p.ImportPath)
base.SetExitStatus(1)
}
var firstErrPkg *load.Package // arbitrarily report setup failed error for first error pkg reached in DFS
load.PackageErrors([]*load.Package{p}, func(p *load.Package) {
reportErr(p, p.Error)
if firstErrPkg == nil {
firstErrPkg = p
}
})
if firstErrPkg != nil {
reportSetupFailed(firstErrPkg, firstErrPkg.Error)
continue
}
buildTest, runTest, printTest, perr, err := builderTest(b, ctx, pkgOpts, p, allImports[p], writeCoverMetaAct)
if err != nil {
reportErr(perr, err)
reportSetupFailed(perr, err)
continue
}
builds = append(builds, buildTest)
runs = append(runs, runTest)
prints = append(prints, printTest)
}
// Order runs for coordinating start JSON prints.
ch := make(chan struct{})
close(ch)
for _, a := range runs {
if r, ok := a.Actor.(*runTestActor); ok {
r.prev = ch
ch = make(chan struct{})
r.next = ch
}
}
// Ultimately the goal is to print the output.
root := &work.Action{Mode: "go test", Actor: work.ActorFunc(printExitStatus), Deps: prints}
// Force the printing of results to happen in order,
// one at a time.
for i, a := range prints {
if i > 0 {
a.Deps = append(a.Deps, prints[i-1])
}
}
// Force benchmarks to run in serial.
if !testC && (testBench != "") {
// The first run must wait for all builds.
// Later runs must wait for the previous run's print.
for i, run := range runs {
if i == 0 {
run.Deps = append(run.Deps, builds...)
} else {
run.Deps = append(run.Deps, prints[i-1])
}
}
}
b.Do(ctx, root)
}
args即命令行输入的全部参数.runTest首先会分析所有需要测试的包.为每
个待测试包生成一个二进制文件.然后执行.
执行流程:
1).参数解析与初始化:
2).关键参数合法性校验:
3).超时/缓存初始化:
4).覆盖率/模糊测试初始化(编译器处理):
5).构建测试二进制+调度执行:
测试动作依赖关系图:
5.运行模式:
1).本地目录模式:
当执行测试没有指定package时.即以本地目录形式运行.例如使用go test
或者 go test -v来启动测试.
在本地目录模式下.go test编译当前目录源码文件和测试文件.并生成一个
二进制文件.最后执行打印结果.
2).包列表模式:
当执行测试并显示指定package时.即以包列表模式运行.例如使用go test
math来启动测试.在包列表模式下.go test为每个包生成一个测试二进制
文件.并分别执行它.它会把每个包的测试结果写入本地临时文件中作为缓
存.下次执行会直接从缓存中读取测试结果.以便节省测试时间.
6.缓存机制:
当满足一定条件时.测试的缓存是自动启用的.也可以显示的关闭缓存.
1).测试结果缓存:
如果一次测试中.其参数全部来自可缓存参数集合.那么本次测试结果将被缓
存.
可缓存参数集合:
-cpu -list -parallel -run -short -v.
测试函数必须全部来自这个集合.其结果才会被缓存.没有参数或包含任一此
集合之外的参数.结果都不会被缓存.
2).使用缓存结果:
如果满足条件.那么测试不会真正的执行.而是从缓存中取出结果并呈现.结
果中会有"cached"字样.表示来自缓存.
缓存结果满足条件:
本次测试的二进制文件及测试函数与之前一次完全一致.
本次测试的源码文件及环境变量与之前的一次完全一致.
之前的一次测试结果是成功的.
本次测试运行模式是列表模式.
3).禁用缓存:
测试使用一个不在可缓存参数集合中的参数.就不会使用缓存.比较常用的是
指定一个参数-count=1.
语雀地址www.yuque.com/itbosunmian…?
《Go.》 密码:xbkk 欢迎大家访问.提意见.
厌倦了争辩.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路