golang cmd test framework
缘由
其实测试 go
本身并不是一件简单的事情,比如我们修改了一部分go源码,如何保证这个修改没有问题呢,除了简单的单元测试之外,最好的方法可能就是执行一下 go build
, 看看是不是能得到自己想要的结果。
最早的 cmd/go
tests 是一个主要由 shell 脚本完成的一些函数组成的库。这些测试写起来非常方便,但是运行起来却非常困难:你不能用 -run
单独的运行某一些tests,它们不能运行在 Windows
上。并且这些测试跑起来非常慢,等等吧。
后来, 随着 CL10464 的合并,一种新的 go 测试框架被引入,之前的 shell 测试脚本被逐步翻译成单独的 go test,这使得这些测试可以被选择性的执行,在 Windows
上执行,并行的执行这些测试,对于golang来说,这真的是一个不小的进步。
不过这些测试写起来还是有点难受,让我们来看一下,下面是最初在 test.bash
里面的一个测试:
TEST 'file:line in error messages'
# Test that error messages have file:line information at beginning of
# the line. Also test issue 4917: that the error is on stderr.
d=$(TMPDIR=/var/tmp mktemp -d -t testgoXXX)
fn=$d/err.go
echo "package main" > $fn
echo 'import "bar"' >> $fn
./testgo run $fn 2>$d/err.out || true
if ! grep -q "^$fn:" $d/err.out; then
echo "missing file:line in error message"
cat $d/err.out
ok=false
fi
rm -r $d
最终的 go 版本的测试是这样的:
func TestFileLineInErrorMessages(t *testing.T) {
tg := testgo(t)
defer tg.cleanup()
tg.parallel()
tg.tempFile("err.go", `package main; import "bar"`)
path := tg.path("err.go")
tg.runFail("run", path)
shortPath := path
if rel, err := filepath.Rel(tg.pwd(), path); err == nil && len(rel) < len(path) {
shortPath = rel
}
tg.grepStderr("^"+regexp.QuoteMeta(shortPath)+":", "missing file:line in error message")
}
我们可以看到好很多了,但是仍然有一点 difficult to skim.
new test framework
聪明的 golang team 又想出了一种新的方法,和以前写一个个小的测试脚本非常类似,但是编写语言不再是shell,是一种 built-for-purpose
和shell非常像的语言。在新的测试框架下,上面的测试将会是一个独立的文件:
testdata/script/fileline.txt:
# look for short, relative file:line in error message
! go run ../../gopath/x/y/z/err.go
stderr ^..[\\/]x[\\/]y[\\/]z[\\/]err.go:
-- ../x/y/z/err.go --
package main; import "bar"
当我们执行 go test cmd/go -run=Script/^fileline$
执行上面的测试时候,我们看看这个测试框架到底怎么运行的呢,首先 go test 会执行 script_test.go 这个文件,这个文件驱动了整个测试框架的运行 (script engine),所有定义在测试脚本中的命令都会在这个文件中实现。比如脚本中 rm
的实现:
// rm removes files or directories.
func (ts *testScript) cmdRm(neg bool, args []string) {
if neg {
ts.fatalf("unsupported: ! rm")
}
if len(args) < 1 {
ts.fatalf("usage: rm file...")
}
for _, arg := range args {
file := ts.mkabs(arg)
removeAll(file) // does chmod and then attempts rm
ts.check(os.RemoveAll(file)) // report error
}
}
txtar
每一个测试脚本都是文本格式的,下面是一个经典的 helloworld
测试脚本:
# hello world
go run hello.go
stderr 'hello world'
! stdout .
-- hello.go --
package main
func main() { println("hello world") }
上面的这个测试脚本其实就是 txtar
格式的,这是为了集成新的测试框架而专门引入的一种新的文件格式,可以在 CL 123359
中看到详细的说明。每一个这样的脚本会被当做是 subtest
, 所以它们仍然可以被go独立执行。
这些脚本默认情况下会被很好的隔离开,所有的脚本都被看成是可以并行的 subtest
, 这些脚本没有 cmd/go
源码目录的访问权限,甚至没有cmd/go/testdata
目录的访问权限,所以它们不能修改这些地方的文件,不能在这些地方创建临时目录。
每一个测试脚本在运行时都会创建一个属于自己的全新的临时目录,你可以使用 $WORK
这个环境变量来使用这个目录,当然还定义了很多的默认环境变量以供编写测试的时候使用:
GOARCH=<target GOARCH>
GOCACHE=<actual GOCACHE being used outside the test>
GOOS=<target GOOS>
GOPATH=$WORK/gopath
GOPROXY=<local module proxy serving from cmd/go/testdata/mod>
GOROOT=<actual GOROOT>
HOME=/no-home
PATH=<actual PATH>
TMPDIR=$WORK/tmp
devnull=<value of os.DevNull>
goversion=<current Go version; for example, 1.12>
脚本中以 #
开头的当然是注释了,用来说明这个测试的作用。!
前缀用来表明这个命令必须失败,如果命令执行成功了,那么这个测试将会失败,当然了也不是每个命令都支持这个前缀的。
txtar中定义的所有命令如下:
- cd dir
进入某个目录
- cmp file1 file2
比较两个文件
- cmpenv file1 file2
和cmp很类似,但是它会替换文件中的环境变量
- cp src... dst
复制文件到目标
- env [key=value...]
定义环境变量,如果没有参数,将列出所有环境变量,这在debug的时候非常有用
- [!] exec program [args...] [&]
执行给定的命令,需要说明的是exec不会结束这个脚本
如果后缀有&符号,那么这个命令将被放在后台执行,一般搭配wait来使用。
- [!] exists [-readonly] file...
判断某个文件是否存在,如果添加了-readonly那么这个文件应该是不可写的
- [!] go args... [&]
执行go命令
- [!] grep [-count=N] pattern file
文件内容需符合grep条件,-count=N参数表明必须有N条匹配的数据在文件中。
- mkdir path...
创建文件夹
- rm file...
删除文件
- skip [message]
跳过测试
- [!] stale path...
给定路径的包名被go认定为 "stale"
- [!] stderr [-count=N] pattern
从标准错误中grep相应的数据,和grep类似
- [!] stdout [-count=N] pattern
从标准输出中grep相应的数据,和grep类似
- stop [message]
停止执行test,会被标记为test pass
- symlink file -> target
创建软连接文件
- wait
等待所有的后台执行完成