简单并且失败过的测试是可信赖的。
当我在大约十年前开始使用 TDD 时,真的很难说服程序员相信单元测试是有价值的。现在就好很多了,但很多软件还是在没有测试覆盖的情况下被生产出来。关于单元测试最常见的讨论从这个问题开始:“你怎么知道你的代码能用?”
这是一个非常好的问题,最好的答案之一是你应该用自动化测试(单元测试)覆盖代码。
那么,你怎么知道你的单元测试是有效的?
测试监视被测系统的正确性,那谁来监视监视者呢?你是否应该再写一些单元测试来测试你的单元测试?这是鸡生蛋蛋生鸡的问题吗?
显然,我们不这么做。那我们为什么要相信单元测试呢?
我认为有两个不同的原因:
很多代码之所以没有按预想运行是因为太复杂了。有时候也会有一些细微的缺陷,但我认为最常见的问题是,代码涉及的内容越多,你就越难把它记在脑子里。一个简单的 Hello World 应用很容易理解。典型的软件则不然。
单元测试往往要简单得多。首先,单元测试应该是确定性的。这意味着每个测试用例应该有一个清晰的路径。换句话说,一个单元测试的圈复杂度应该是 1。
如果你遵循 AAA 或四阶段测试模式,保持循环复杂度为 1,持续减少代码行数,并使用好的名称,就会有一个可读的测试用例。这样的测试代码很容易审查其正确性。如果你在结对编程,你和你的伙伴在写单元测试代码的时候就进行审查。或者通过其他的审查机制(例如 pull requests)审查你的单元测试代码。即使没有其他人审查你的代码,保持简单也有助于你理解自己做了什么。归根结底,相比没有测试的编写生产代码,有测试更不容易出错。
我们信任单元测试的部分原因是,如果写得好,它们很容易审查正确性。
深深根植在 TDD 中的是红/绿/重构循环。见到测试失败极其重要。就个人而言,这个规则大约每周都会救我一次。我写了一个测试并运行,结果是出乎我的意料的假阴性。虽然我使用 TDD 已经十几年了,但这种情况还是经常发生在我身上,所以我认为应该一直严格遵守这个规则。见到测试失败,就验证了测试确实测试了一些东西。
这也是为什么如果你不用 TDD,而是在事后写测试,那么遵循一个适当的程序来编写特征测试就变得极为重要。
我们信任单元测试的部分原因是我们已经看到了单元测试的失败。就像复式记账。测试使被测系统不偏离原状,而被测系统使测试通过。
我提供的这两个理由都是集中在测试建立的时候。建立测试时你看到它失败了,你会审查它的正确性。随着时间的推移,你最终会得到一个有很多测试的测试套件。你看过这些测试吗?
你相信它们,因为它们在创建时是正确的。它们在今天还正确吗?
好吧,如果你再也不碰它们的话,它们可能还是正确的,但是每次你修改一个测试用例,你就会让它的可信度降低一点。和开放/封闭原则的思想类似,一个测试套件应该向扩展开放,向修改封闭。可信的测试套件是仅可扩展的。