gopl-zh.github.com/ch11/ch11-02-5.md
2015-12-16 10:22:56 +08:00

3.6 KiB
Raw Blame History

11.2.5. 編寫有效的測試

许多Go新人会惊异与它的极简的测试框架. 很多其他语言的测试框架都提供了识别测试函数的机制(通常使用反射或元数据), 通过设置一些 setupteardown 的钩子函数来执行测试用例运行的初始化或之后的清理操作, 同时测试工具箱还提供了很多类似assert断言, 比较值, 格式化输出错误信息和停止一个识别的测试等辅助函数(通常使用异常机制). 虽然这些机制可以使得测试非常简洁, 但是测试输出的日志却像火星文一般难以理解. 此外, 虽然测试最终也会输出 PASS 或 FAIL 的报告, 但是它们提供的信息格式却非常不利于代码维护者快速定位问题, 因为失败的信息的具体含义是非常隐患的, 比如 "assert: 0 == 1" 或 成页的海量跟踪日志.

Go语言的测试风格则形成鲜明对比. 它期望测试者自己完成大部分的工作, 定义函数避免重复, 就像普通编程那样. 编写测试并不是一个机械的填充过程; 一个测试也有自己的接口, 尽管它的维护者也是测试仅有的一个用户. 一个好的测试不应该引发其他无关的错误信息, 它只要清晰简洁地描述问题的症状即可, 有时候可能还需要一些上下文信息. 在理想情况下, 维护者可以在不看代码的情况下就能根据错误信息定位错误产生的原因. 一个好的测试不应该在遇到一点小错误就立刻退出测试, 它应该尝试报告更多的测试, 因此我们可能从多个失败测试的模式中发现错误产生的规律.

下面的断言函数比较两个值, 然后生成一个通用的错误信息, 并停止程序. 它很方便使用也确实有效果, 但是当识别的时候, 错误时打印的信息几乎是没有价值的. 它并没有为解决问题提供一个很好的入口.

import (
	"fmt"
	"strings"
	"testing"
)
// A poor assertion function.
func assertEqual(x, y int) {
	if x != y {
		panic(fmt.Sprintf("%d != %d", x, y))
	}
}
func TestSplit(t *testing.T) {
	words := strings.Split("a:b:c", ":")
	assertEqual(len(words), 3)
	// ...
}

从这个意义上说, 断言函数犯了过早抽象的错误: 仅仅测试两个整数是否相同, 而放弃了根据上下文提供更有意义的错误信息的做法. 我们可以根据具体的错误打印一个更有价值的错误信息, 就像下面例子那样. 测试在只有一次重复的模式出现时引入抽象.

func TestSplit(t *testing.T) {
	s, sep := "a:b:c", ":"
	words := strings.Split(s, sep)
	if got, want := len(words), 3; got != want {
		t.Errorf("Split(%q, %q) returned %d words, want %d",
			s, sep, got, want)
	}
	// ...
}

现在的测试不仅报告了调用的具体函数, 它的输入, 和结果的意义; 并且打印的真实返回的值和期望返回的值; 并且即使断言失败依然会继续尝试运行更多的测试. 一旦我们写了这样结构的测试, 下一步自然不是用更多的if语句来扩展测试用例, 我们可以用像 IsPalindrome 的表驱动测试那样来准备更多的 s, sep 测试用例.

前面的例子并不需要额外的辅助函数, 如果如果有可以使测试代码更简单的方法我们也乐意接受. (我们将在 13.3节 看到一个 reflect.DeepEqual 辅助函数.) 开始一个好的测试的关键是通过实现你真正想要的具体行为, 然后才是考虑然后简化测试代码. 最好的结果是直接从库的抽象接口开始, 针对公共接口编写一些测试函数.

练习11.5: 用表格驱动的技术扩展TestSplit测试, 并打印期望的输出结果.