Go进阶之Main测试的实现原理和go test运行

0 阅读8分钟

每种测试(单元测试 性能测试或示例测试)都有一个数据类型与其对应.

单元测试: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 欢迎大家访问.提意见.

厌倦了争辩.





如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路