前言

Go 支持测试的量大要素:

  • go test 命令
  • 以及 testing 包

要编写测试集,需要创建一个包含 TestXXXfuncxxx_test.go 文件,将则个文件放在与被测包相同的包下面。

编译被测包时,该文件将被排除在外,而执行 go test 时,该文件将被包含在内。

包内测试

由于 Go 构建工具链在编译包时会自动根据文件名是否具有_test.go后缀将包源文件和包的测试源文件分开,测试代码不会进入包正常构建的范畴,因此测试代码使用与被测包名相同的包内测试方法是一个很自然的选择。

包内测试这种方法本质上是一种白盒测试方法。由于测试代码与被测包源码在同一包名下,测试代码可以访问该包下的所有符号,无论是导出符号还是未导出符号;

并且由于包的内部实现逻辑对测试代码是透明的,包内测试可以更为直接地构造测试数据和实施测试逻辑,可以很容易地达到较高的测试覆盖率。因此对于追求高测试覆盖率的项目而言,包内测试是不二之选。

但实际使用时,会遇到如下问题:

  • 因为更高的测试覆盖率,导致测试代码的耦合层度较高,以至于,被测代码的改动会影响到测试用例的代码;
  • 包的循环引用,如 A 引用 B,B 的测试用例中又引用 A,这就会导致循环引用,最终无法通过编译;

包外测试

为了解决包内测试的循环引用问题,便引入了包外测试。与包内测试本质是面向实现的白盒测试不同,包外测试的本质是一种面向接口的黑盒测试。

这里的“接口”指的就是被测试包对外导出的API,这些API是被测包与外部交互的契约。

契约一旦确定就会长期保持稳定,无论被测包的内部实现逻辑和数据结构设计如何调整与优化,一般都不会影响这些契约。

这一本质让包外测试代码与被测试包充分解耦,使得针对这些导出API进行测试的包外测试代码表现出十分健壮的特性,即很少随着被测代码内部实现逻辑的调整而进行调整和维护。

不过包外测试的不足也是显而易见的,那就是存在测试盲区。由于测试代码与被测试目标并不在同一包名下,测试代码仅有权访问被测包的导出符号,并且仅能通过导出 API 这一有限的“窗口”并结合构造特定数据来验证被测包行为。

在这样的约束下,很容易出现对被测试包的测试覆盖不足的情况。

Go 标准库的实现者们提供了一个解决这个问题的惯用法:

安插后门,这个后门就是通过使用 export_test.go 文件,该文件中的代码位于被测包名下,但它既不会被包含在正式产品代码中(因为位于_test.go文件中),又不包含任何测试代码,而仅用于将被测包的内部符号在测试阶段暴露给包外测试代码。

// export_test.go
package repository

var Client = client
var IsAdmin = isAdmin

这样就能在 repository_test 这个包中获取 repository 包内的内部符号了!

经过上面的比较,我们发现包内测试与包外测试各有优劣,那么在 Go 测试编码实践中我们究竟该选择哪种测试方式呢?关于这个问题,目前并无标准答案。

但是我个人更倾向于包外测试,这种松耦合的方式,以包使用者的视角去测试。如果一定要提测试覆盖率,那么可以采用 export_test.go 这种方式!

I hope this is helpful, Happy hacking…