测试无法防止每一个 bug
对于任何形式的测试,一个非常古老的反对意见是:你无法捕捉到每一个错误。虽然这一点无疑是正确的,但恰恰意味着我们需要更多、更好的测试,而不是减少。让我们深入理解这一观点背后的动机,以便准备一个恰当的回应。
理解为什么人们说测试无法捕捉每个 bug
我们首先可以同意这一观点:测试确实无法捕捉到每一个错误。更准确地说,已经证明在软件系统中,测试只能揭示缺陷的存在,而永远无法证明不存在任何缺陷。即使我们有许多通过的测试,缺陷仍可能隐藏在我们未测试的地方。
这一现象似乎也适用于其他领域。医学扫描并不总能发现那些过于微弱而难以察觉的问题。飞机的风洞测试并不总能揭示特定飞行条件下的问题。巧克力工厂的批次抽样也无法捕捉到每一颗不合格的糖果。
仅仅因为我们无法捕捉到每一个错误,并不意味着我们的测试是无效的。我们编写的每一个能够捕捉到一个缺陷的测试,都会减少一个流入我们工作流程的缺陷。测试驱动开发(TDD)为我们提供了一个在开发过程中以测试为导向的思维框架,但仍有一些领域我们的测试可能无法覆盖:
-
我们没有想到要编写的测试
-
由于系统级交互而产生的缺陷
未编写的测试是一个实际问题。即使在 TDD 中先写测试,我们也必须有足够的纪律性,为我们希望实现的每一个场景编写测试。编写一个测试然后编写代码使其通过是容易的,但诱惑在于我们会因为进展顺利而继续添加代码,从而容易忽略一些边缘情况,进而未为其编写测试。如果我们遗漏了测试,就有可能存在缺陷并在后期被发现。
系统级交互的问题指的是,当你将经过测试的软件单元组合在一起时,可能会出现的行为。单元之间的交互有时可能比预期的更为复杂。基本上,即使我们将两个经过充分测试的单元组合在一起,新的组合本身仍然未经测试。某些交互中的问题只会在这些交互中显现,即使组成它们的单元已经通过了所有测试。
这两个问题是真实且合理的。测试永远无法覆盖每一个可能的故障,但这忽略了测试的主要价值。我们编写的每一个测试都会减少一个缺陷。
如果不进行任何测试,我们将永远无法发现任何问题,也无法预防任何缺陷。如果我们进行测试,无论多么少,都会提高代码的质量。这些测试能够检测到的每一个缺陷都将被预防。我们可以看到这一论点的 “稻草人” 性质:仅仅因为我们无法覆盖所有可能性,并不意味着我们不应该尽力而为。
克服不捕捉每个 bug 的异议
重新审视这一问题的关键在于,我们要有信心认识到测试驱动开发(TDD)能够预防许多类别的错误。虽然它确实无法预防所有类型的错误,但成千上万的测试用例将显著提升我们应用程序的质量。
为了向同事们解释这一点,我们可以借助一些熟悉的类比:仅仅因为强密码无法阻止每一个黑客,并不意味着我们不应该使用密码,从而让自己对任何黑客都毫无防备。保持健康并不能预防每一种疾病,但它可以预防许多严重的健康问题。
归根结底,这是一个平衡的问题。完全不进行测试显然是不够的——在这种情况下,每一个缺陷最终都会上线。我们知道测试永远无法完全消除缺陷,那么我们应该在哪里停止?什么才算是足够的?我们可以认为,TDD 帮助我们在最佳时机——即在我们思考编写代码时——决定这种平衡。我们创建的自动化 TDD 测试将节省手动 QA 的时间,这些不再需要手动完成的工作会带来时间和成本的节省,并在每一次代码迭代中为我们带来回报。
既然我们已经理解了为什么尽可能多地进行测试总是比完全不测试要好,接下来我们可以探讨另一个常见的反对意见:我们如何知道测试本身是否编写正确?