Go 测试提示和技巧

GO

刘宇帅 10148 0 7年前

文章内容

原文地址:

https://medium.com/@povilasve/go-advanced-tips-tricks-a872503ac859

这篇文章是基于维尔纽斯Go见面会做的整理。
我看了很多博客并把他们内容整理以下。首先我要感谢哪些收集所有的想法并在社区分享给大家。我这篇文章有用到下面文章的内容和例子:

技巧一:不要用框架

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联系我。

专栏信息

GO
作者 刘宇帅
发布时间 7年前
阅读量 10148
评论数 0

摘要


暂无评论~

更多专栏文章

Go 中如何使用 slice 的长度和容量

原文地址:How to use slice capacity and length in Go 简短测试 - 下面代码输出结果是什么? vals := make([]int, 5) for i:=0; i < 5; i++ { vals = append(vals, i) } fmt.Println(vals) 如果你猜是 [0 0 0 0 0 0 1 2 3 4],那么你是对的。 什么?为什么不是[0 1 2

查看文章

Go 是按值传递的为什么可以修改 slice 的值

原文地址:Why are slices sometimes altered when passed by value in Go? 在我的上篇博客中我们讨论了array 和 slice 的一些不同之处。换句话说,我们讨论了为什么slice 既有长度也有容量,但是array只有长度。我们也简短地介绍了slice是如何使用

查看文章

Go 编译时断言

原文地址:Go 编译时断言 这篇文章介绍一个鲜为人知的在Go中编译时断言的方法。你可能不会用到它,但是了解下也是非常有趣的。 作为热身,这是一个相当知名的编译时断言的方法:接口满足性检查。 在下面的代码中(playgro

查看文章

GO Range内幕

原文地址:Go Range Loop Internals Go range是非常方便的,但是我总感觉它非常的神秘。不只是我这样认为: #golang pop quiz: does this program terminate? func main() { v := []int{1, 2, 3} for i := range v { v = append(v, i) } } — Dαve Cheney (@davecheney) January 13, 20

查看文章

golang 如何优雅的关闭 channels

原文地址:How To Gracefully Close Channels 几天前我写了一篇介绍 GO 语言 channel 的文章,文章在reddit和HN上收到很多赞。 我搜集了一些关于 Go channels 设计和规则方面的评论: 除了主动去关闭一个 channel 外,并没有一种简单并且通用的

查看文章