gopl-zh.github.com/ch11/ch11-02-5.md

3.7 KiB
Raw Blame History

11.2.5. 编写有效的测试

许多Go语言新人会惊异于它的极简的测试框架。很多其它语言的测试框架都提供了识别测试函数的机制通常使用反射或元数据通过设置一些“setup”和“teardown”的钩子函数来执行测试用例运行的初始化和之后的清理操作同时测试工具箱还提供了很多类似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测试并打印期望的输出结果。