设为首页 加入收藏

TOP

[译] Go语言测试进阶版建议与技巧(一)
2019-09-17 14:36:14 】 浏览:107
Tags:语言 测试 进阶 建议 技巧

阅读本篇文章前,你最好已经知道如何写基本的单元测试。本篇文章共包含3个小建议,以及7个小技巧。

建议一,不要使用框架

Go语言自身已经有一个非常棒的测试框架,它允许你使用Go编写测试代码,不需要再额外学习其它的库或测试引擎。关于断言方面的帮助函数,你可以看看这个 testing,或者这个 assert.go :)

建议二,使用"_test"包名

相较于直接使用被测试代码的包名,使用 *_test包名使得测试代码只能访问包中对外暴露出的接口。这使得你在写测试时更多的是站在包使用者的角度来写,从而使得你可以思考包的接口是否设计合理。

建议三,避免全局常量配置项

避免使用全局常量配置项,因为测试代码无法修改常量。下面举了三个例子做对比:

// 1. 不好,测试代码无法修改它
const port = 8080

// 2. 好一些,测试代码可以修改它
var port = 8080

// 3. 更好的方式,测试代码可以通过 struct 配置 Port
const defaultPort = 8080
type AppConfig {
  Port int // 构造函数中初始化为 defaultPort
}

技巧一,加载测试数据

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文件中

将测试时的预期结果保存至.golden文件中。并且提供一个flag来决定是否更新它。使用这个技巧可以避免在测试代码中硬编码预期输出结果非常复杂的内容。看例子:

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!
  }
}

技巧三,测试时的初始化、清理代码

有时候测试代码比较复杂,在跑测试的case之前需要初始化好环境,这可能会包含很多不相关的错误检查,比如测试文件是否加载成功,测试数据是否能按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")()
  // ...
}

上面的例子包含了另外一个关于defer使用的非常酷的技巧。defer testChdir(t, "/other")()会先执行testChdir内的代码,并且在TestThing结束时执行testChdir所返回的清理函数中的代码。

技巧四,当依赖第三方可执行程序时

有时测试代码会依赖第三方可执行程序,我们可以通过以下方法检查程序是否存在,存在则执行测试,不存在则跳过测试。

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()
  }
  // ...
}

技巧五,测试包含os.Exit的代码

该方法通过启动子进程的方式,避免测试包含os.Exit的代码导致测试程序提前退出。看例子:

func CrashingGit() {
  os.Exit(1)
}
func TestFailingGit(t *testing.T) {
  if os.Getenv("BE_CRASHING_GIT") == "1" { // 子进程进入这个逻辑分支
    CrashingGit()
    return
  }
  // 被 go test 执行,
  // 设置好环境变量,启动子进程再次执行 TestFailingGit
  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)
}

上面例子的思想是,当Go测试框架运行TestFailingGit时,启动一个子进程(os.Args[0]即生成的Go测试程序)。子进程再次运行测试程序,并只执行TestFailingGit(通过参数 -test.run=TestFailingGit 实现),并且设置了环境变量BE_CRASHING_GIT=1,这样子进程将执行CrashingGit()

技巧六,将mocks、helpers放入testing.go文件中

testing.go文件会被当做一个普通的源码文件,而不是测试代码文件。这样在其它的包中或其

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Go语言【数据结构】切片 下一篇2.GO-可变参数函数,匿名函数和函..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目