Go 测试提示和技巧
GO 刘宇帅 6年前 阅读量: 8435
原文地址:
https://medium.com/@povilasve/go-advanced-tips-tricks-a872503ac859
这篇文章是基于维尔纽斯Go见面会做的整理。
我看了很多博客并把他们内容整理以下。首先我要感谢哪些收集所有的想法并在社区分享给大家。我这篇文章有用到下面文章的内容和例子:
- Andrew Gerrand — Testing Techniques
- Mitchell Hashimoto — Advanced Testing with Go
- https://medium.com/@benbjohnson/structuring-tests-in-go-46ddee7a25c#.q88391hne
- Dave Cheney — Test Fixtures in Go
- Peter Bourgon — Go: Best Practices for Production Environments 开始读下面之前,我建议你在阅读之前先要知道怎么做表格测试和接口mock测试。现在让我们开始学习技巧:
技巧一:不要用框架
Ben Johnson的技巧。Go本身的测试框架很好用,你可以用Go本身来写测试而且也不依赖任何框架和引擎。也可以看看Ben Johnson的帮助函数,可以帮助你节省不少代码。
技巧二:使用带”_test“的测试包
Ben Johnson的技巧。使用”_test“的包不允许你调用未对外导出的标识符。这把测试放在了单独的包,这样你可以用来测试包对外开放的api非常有用。
避免全局变量
Mitchell Hashimoto的技巧。如果你使用全局常量那么你无法配置或者修改相应变量。例外情况是全局变量是被用来做默认值的情况。看下下面的例子:
// Bad, tests cannot change value!
const port = 8080
// Better, tests can change the value.
var port = 8080
// Even better, tests can configure Port via struct.
const defaultPort = 8080
type AppConfig {
Port int // set it to defaultPort using constructor.
}
下面是一些技巧希望能让你的测试更好
技巧一:测试套件
这个技巧在标准连接库里别使用到了。我是在Mitchell Hashimoto和 Dave Cheney文章里学到的。Go测试很好的支持从文件中加载数据。首先,Go构建会忽略文件夹testData,第二,Go跑测试的时候把当前目录当作包目录。这允许你使用相对路径从testData目录加载或者存储数据。下面是一个例子:
func helperLoadBytes(t *testing.T, name string) []byte {
path := filepath.Join("testdata", name) // relative path
bytes, err := ioutil.ReadFile(path)
if err != nil {
t.Fatal(err)
}
return bytes
}
技巧二:Golden files
这个技巧也是在标准连接库里被用到,但是我是从Mitchell Hashimoto的文章里学到的。具体方式就是把期待的输出结果保存为以.golden结尾的文件并提供一个参数可以用来标示更新相应文件。下面是一个例子:
var update = flag.Bool("update", false, "update .golden files")
func TestSomething(t *testing.T) {
actual := doSomething()
golden := filepath.Join(“testdata”, tc.Name+”.golden”)
if *update {
ioutil.WriteFile(golden, actual, 0644)
}
expected, _ := ioutil.ReadFile(golden)
if !bytes.Equal(actual, expected) {
// FAIL!
}
}
这个技巧可以让你不用硬编码的方式测试复杂的应用。
技巧三:测试帮助函数
Mitchell Hashimoto的测试技巧。有时候测试代码比较复杂,当你为你的代码准备适当的测试用例的时候,经常需要去处理很多无关的错误检查,例如检查测试文件是否加载,检查传过来的参数是否是json等等... 这样会让代码越来越复杂!为了解决这种问题,我们应该把这些代码拆分到帮助函数里。帮助函数应该永远不要返回error,而是把相应的错误通过testing.T报告具体的错误。
另外,如果你的帮助函数在结束后需要做清理工作,你应该放回一个函数来执行具体的清理。下面是一个例子:
func testChdir(t *testing.T, dir string) func() {
old, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(dir); err != nil {
t.Fatalf("err: %s", err)
}
return func() {
if err := os.Chdir(old); err != nil {
t.Fatalf("err: %s", err)
}
}
}
func TestThing(t *testing.T) {
defer testChdir(t, "/other")()
// ...
}
(注:这个例子来自于Mitchell Hashimoto的文章Advanced Testing with Go)。这个例子里的另外一个技巧是defer的使用。上面的例子使用defer testChdir(t, "/other")()调用testChdir函数并延迟执行清理函数。
技巧四:Subprocessing: Real
有时候你的代码依赖可执行文件,例如你的代码依赖于git。测试这个代码的一个方法是mock git,但是这还是挺难的。第二种方法是调用git可执行文件。但是如果没有安装git怎么跑测试呢?这个技巧就是通过检查系统是否有git否者跳过相应的测试。下面是一个例子:
var testHasGit bool
func init() {
if _, err := exec.LookPath("git"); err == nil {
testHasGit = true
}
}
func TestGitGetter(t *testing.T) {
if !testHasGit {
t.Log("git not found, skipping")
t.Skip()
}
// ...
}
(这个例子来自 Mitchell Hashimoto的文章 Advanced Testing with Go)
技巧五:Subprocessing: Mock
Andrew Gerrand 和 Mitchell Hashimoto的测试技巧。这个技巧让你在测试代码里mock一个子进程。这个想法在标准库测试里可以看到。假定我们要测试git失败的场景。我们看下下面的例子:
func CrashingGit() {
os.Exit(1)
}
func TestFailingGit(t *testing.T) {
if os.Getenv("BE_CRASHING_GIT") == "1" {
CrashingGit()
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestFailingGit")
cmd.Env = append(os.Environ(), "BE_CRASHING_GIT=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
return
}
t.Fatalf("Process ran with err %v, want os.Exit(1)", err)
}
这个例子是使用轻微的修改用子程序跑测试框架。轻微的修改是运行同样的命令(-test.run=TestFailingGit part),但是会设一个环境变量BE_CRASHING_GIT=1,这个变量可以用来区分测试是在正常执行还是子进程执行。
技巧六:把mocks、帮助函数放到testing.go文件
Hashimoto给了一个有趣的建议,建议我们把帮助函数、fixtures、stubs exported放到testing.go文件。(注:testing.go是正常的文件,不会被当作测试文件)这样你就可以在不同的包里使用你的mocks和帮助函数,其他人也可以在测试里使用你的代码。
关注慢测试
Peter Bourgon的测试技巧。如果你有比较慢的测试,那么等待他们执行完成很烦人,尤其是你想知道是否可以构建应用的时候。解决办法就是把慢的测试放到_integration_test.go文件中并在文件开头添加标签。例如:
// +build integration
然后你在执行go test就不回去执行这些慢的测试了。如果你想执行这些慢的测试需要添加构建参数:
go test -tags=integration
就我而言,我使用了命令别名用来执行当前目录和子目录但是除了vendor目录外的所有测试
alias gtest="go test \$(go list ./… | grep -v /vendor/)
-tags=integration"
别名也支持简洁的参数:
$ gtest
...
$ gtest -v
...
谢谢你的阅读!如果你有问题或者给我一些反馈,你可以通过我的博客 https://povilasv.me或者twitter@PofkeVe联系我。