前言
对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
文件,然后执行,执行中会添加默认参数,这些参数如不满足需求可以调整。