Go test之理

前言

对func 进行test时,可以使用命令

go test -run func_name

例如对Example进行test,即执行

go test -run Example

我们知道go程序的入口是main.main,必须有main.main程序才能正常运行。那go test是如何运行的呢?答案是生成main.main,然后再调用test func。

更多内容分享,欢迎关注公众号:Go开发笔记

生成main

先看生成模板。

// go1.18
var testmainTmpl = lazytemplate.New("main", `
// Code generated by 'go test'. DO NOT EDIT.

package main

import (
	"os"
{{if .TestMain}}
	"reflect"
{{end}}
	"testing"
	"testing/internal/testdeps"

{{if .ImportTest}}
	{{if .NeedTest}}_test{{else}}_{{end}} {{.Package.ImportPath | printf "%q"}}
{{end}}
{{if .ImportXtest}}
	{{if .NeedXtest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{end}}
{{if .Cover}}
{{range $i, $p := .Cover.Vars}}
	_cover{{$i}} {{$p.Package.ImportPath | printf "%q"}}
{{end}}
{{end}}
)

var tests = []testing.InternalTest{
{{range .Tests}}
	{"{{.Name}}", {{.Package}}.{{.Name}}},
{{end}}
}

var benchmarks = []testing.InternalBenchmark{
{{range .Benchmarks}}
	{"{{.Name}}", {{.Package}}.{{.Name}}},
{{end}}
}

var fuzzTargets = []testing.InternalFuzzTarget{
{{range .FuzzTargets}}
	{"{{.Name}}", {{.Package}}.{{.Name}}},
{{end}}
}

var examples = []testing.InternalExample{
{{range .Examples}}
	{"{{.Name}}", {{.Package}}.{{.Name}}, {{.Output | printf "%q"}}, {{.Unordered}}},
{{end}}
}

func init() {
	testdeps.ImportPath = {{.ImportPath | printf "%q"}}
}

{{if .Cover}}

// Only updated by init functions, so no need for atomicity.
var (
	coverCounters = make(map[string][]uint32)
	coverBlocks = make(map[string][]testing.CoverBlock)
)

func init() {
	{{range $i, $p := .Cover.Vars}}
	{{range $file, $cover := $p.Vars}}
	coverRegisterFile({{printf "%q" $cover.File}}, _cover{{$i}}.{{$cover.Var}}.Count[:], _cover{{$i}}.{{$cover.Var}}.Pos[:], _cover{{$i}}.{{$cover.Var}}.NumStmt[:])
	{{end}}
	{{end}}
}

func coverRegisterFile(fileName string, counter []uint32, pos []uint32, numStmts []uint16) {
	if 3*len(counter) != len(pos) || len(counter) != len(numStmts) {
		panic("coverage: mismatched sizes")
	}
	if coverCounters[fileName] != nil {
		// Already registered.
		return
	}
	coverCounters[fileName] = counter
	block := make([]testing.CoverBlock, len(counter))
	for i := range counter {
		block[i] = testing.CoverBlock{
			Line0: pos[3*i+0],
			Col0: uint16(pos[3*i+2]),
			Line1: pos[3*i+1],
			Col1: uint16(pos[3*i+2]>>16),
			Stmts: numStmts[i],
		}
	}
	coverBlocks[fileName] = block
}
{{end}}

func main() {
{{if .Cover}}
	testing.RegisterCover(testing.Cover{
		Mode: {{printf "%q" .Cover.Mode}},
		Counters: coverCounters,
		Blocks: coverBlocks,
		CoveredPackages: {{printf "%q" .Covered}},
	})
{{end}}
	m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, fuzzTargets, examples)
{{with .TestMain}}
	{{.Package}}.{{.Name}}(m)
	os.Exit(int(reflect.ValueOf(m).Elem().FieldByName("exitCode").Int()))
{{else}}
	os.Exit(m.Run())
{{end}}
}

`)

以一个简单例子说明

源文件

// test/gotest.go
package gotest

import (
	"fmt"
	"os"
)

func Example() {
	fmt.Println(os.Args)
	fmt.Println("this is an example.")
}

测试文件

// test/gotest_test.go
func TestExample(t *testing.T) {
	Example()
}

生成文件名_testmain.go


// Code generated by 'go test'. DO NOT EDIT.

package main

import (
	"os"

	"testing"
	"testing/internal/testdeps"


	_test "test/gotest"



)

var tests = []testing.InternalTest{

	{"TestExample", _test.TestExample},

}

var benchmarks = []testing.InternalBenchmark{

}

var fuzzTargets = []testing.InternalFuzzTarget{

}

var examples = []testing.InternalExample{

}

func init() {
	testdeps.ImportPath = "test/gotest"
}



func main() {

	m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, fuzzTargets, examples)

	os.Exit(m.Run())

}

可以看到生成的_testmain.go中引入了测试package,存入testdeps.ImportPath,package内所有测试func存入tests中,最后构建MainStart及run。

MainStart

// MainStart is meant for use by tests generated by 'go test'.
// It is not meant to be called directly and is not subject to the Go 1 compatibility document.
// It may change signature from release to release.
func MainStart(deps testDeps, tests []InternalTest, benchmarks []InternalBenchmark, fuzzTargets []InternalFuzzTarget, examples []InternalExample) *M {
	Init() // 取flag对应的参数
	return &M{
		deps:        deps,
		tests:       tests,
		benchmarks:  benchmarks,
		fuzzTargets: fuzzTargets,
		examples:    examples,
	}
}

// Run runs the tests. It returns an exit code to pass to os.Exit.
func (m *M) Run() (code int) {
	defer func() {
		code = m.exitCode
	}()

    ...

	// Run tests, examples, and benchmarks unless this is a fuzz worker process.
	// Workers start after this is done by their parent process, and they should
	// not repeat this work.
	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 !testOk || !exampleOk || !fuzzTargetsOk || !runBenchmarks(m.deps.ImportPath(), m.deps.MatchString, m.benchmarks) || race.Errors() > 0 {
			fmt.Println("FAIL")
			m.exitCode = 1
			return
		}
	}

	...
	return
}


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
			}
			if i > 0 && !ran {
				// There were no tests to run on the first
				// iteration. This won't change, so no reason
				// to keep trying.
				break
			}
			ctx := newTestContext(*parallel, newMatcher(matchString, *match, "-test.run")) // 生成正则表达式matcher
			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
}


func (t *T) Run(name string, f func(t *T)) bool {
	atomic.StoreInt32(&t.hasSub, 1)
	testName, ok, _ := t.context.match.fullName(&t.common, name) // 过滤当前func name是否满足matcher,不满足不执行
	if !ok || shouldFailFast() {
		return true
	}
	// Record the stack trace at the point of this call so that if the subtest
	// function - which runs in a separate stack - is marked as a helper, we can
	// continue walking the stack into the parent test.
	var pc [maxStackLen]uintptr
	n := runtime.Callers(2, pc[:])
	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,
		},
		context: t.context,
	}
	t.w = indenter{&t.common}

	if t.chatty != nil {
		t.chatty.Updatef(t.name, "=== RUN   %s\n", t.name)
	}
	// Instead of reducing the running count of this test before calling the
	// tRunner and increasing it afterwards, we rely on tRunner keeping the
	// count correct. This ensures that a sequence of sequential tests runs
	// without being preempted, even when their parent is a parallel test. This
	// may especially reduce surprises if *parallel == 1.
	go tRunner(t, f) // 执行具体的test func
	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()
	}
	return !t.failed
}

最后看下执行时的命令及参数

通过获取os.Args,可以得到示例运行时执行的命令为

[/var/folders/x2/z6mtnt_x7z77b71h_4_mphr00000gn/T/go-build393711866/b001/gotest.test -test.paniconexit0 -test.timeout=10m0s -test.run=Example]

从上可知,go test执行最终会编译成{test_package}.test,运行时会默认添加的参数为:panic退出标志test.paniconexit0,超时时间test.timeout默认10分钟。这些默认参数如不满足需求,可以通过添加参数进行调整。

go test -run func_name -test.timeout=30m0s

go test run相当于

go test -c // 生成可执行文件gotest.test
./gotest.test -test.timeout=30m0s -test.run=Example

注意:在测试程序运行结束前,如果去{test_package}.test所在文件夹,可以发现以下文件(测试运行结束后文件夹不存在,具体原因后续说明)。

importcfg.link //链接依赖库配置
importcfg // 依赖库配置
gotest.test // 生成测试package的.test可执行文件
_testmain.go // 生成的test package入口
_pkg_.a // 测试packge build后生成的静态库文件

_testmain.go{test_package}.test文件就在其中。

总结

本文从一个简单测试例子来说明go test背后的处理过程,简单来说就是根据模板及当前测试package及测试func生成_testmain.go文件,在_testmain.go中完成按测试的调用。在运行时先编译生成{test_package}.test文件,然后执行,执行中会添加默认参数,这些参数如不满足需求可以调整。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值