从测试中学习
我们的测试是设计反馈的丰富来源。当我们做出决策时,我们将它们编写为测试代码。看到这段代码——生产代码的首次使用——使我们能够清晰地看到我们提出的设计有多好。当我们的设计不好时,测试的 AAA 部分会将这些设计问题揭示为测试中的代码异味。让我们详细了解一下这些部分如何帮助识别有问题的设计。
杂乱的 Arrange 步骤
如果我们的 Arrange(准备) 步骤中的代码很混乱,我们的对象可能难以创建和配置。它可能需要在构造函数中使用太多参数,或者在测试中留下太多可选参数为 null
。可能是对象需要注入太多依赖项,表明它承担了太多职责,或者可能需要太多原始数据参数来传递大量配置项。这些信号表明,我们创建对象的方式可能需要进行重新设计。
杂乱的 Act 步骤
在 Act(执行) 步骤中调用代码的主要部分通常很简单,但它可能会揭示一些基本的设计错误。例如,我们可能会传入不明确的参数,例如布尔值或字符串对象的列表。很难知道每个参数的含义。我们可以通过将这些难以理解的参数包装在一个易于理解的新类中(称为配置对象)来重新设计。另一个可能的问题是,如果 Act 步骤需要以特定顺序进行多次调用。这很容易出错。很容易以错误的顺序调用它们或忘记其中一个调用。我们可以重新设计,使用一个包装所有这些细节的单一方法。
杂乱的 Assert 步骤
Assert(断言) 步骤将揭示我们的代码结果是否难以使用。问题可能包括必须以特定顺序调用访问器,或者返回一些传统的代码异味,例如一个结果数组,其中每个索引具有不同的含义。我们可以重新设计以使用更安全的构造。
在每种情况下,我们的单元测试中的某部分代码看起来有问题——它有代码异味。这是因为我们正在测试的代码设计也存在相同的代码异味。这就是单元测试在设计上提供快速反馈的意义。它们是我们编写的代码的第一个用户,因此我们可以及早发现问题区域。
现在,我们已经掌握了开始为我们的示例应用程序编写第一个测试所需的所有技术。让我们开始吧。
单元测试的局限性
一个非常重要的观点是,自动化测试只能证明缺陷的存在,而不能证明缺陷的不存在。这意味着,如果我们想到一个边界条件,为其编写一个测试,并且测试失败,我们就知道我们的逻辑中存在缺陷。然而,如果所有测试都通过,这并不意味着也不能意味着我们的代码没有缺陷。它只意味着我们的代码没有我们想到要测试的所有缺陷。根本没有一种神奇的解决方案可以确保我们的代码没有缺陷。TDD 在这方面给了我们很大的帮助,但我们绝不能因为所有测试都通过就声称我们的代码没有缺陷。这根本是不真实的。
一个重要的结果是,我们的 QA 工程同事仍然像以往一样重要,尽管我们现在帮助他们从一个更容易的起点开始。我们可以将经过 TDD 测试的代码交付给手动 QA 同事,他们可以放心,许多缺陷已经被预防并证明不存在。这意味着他们可以开始进行手动探索性测试,发现我们从未想到要测试的所有内容。通过合作,我们可以使用他们的缺陷报告编写更多的单元测试来纠正他们发现的问题。即使使用 TDD,QA 工程师的贡献仍然至关重要。我们需要团队中的所有帮助来努力编写高质量的软件。
代码覆盖率 - 一项常常无意义的度量
代码覆盖率是衡量在给定运行中执行了多少行代码的指标。它通过检测代码来测量,这是代码覆盖率工具为我们做的事情。它通常与单元测试结合使用,以衡量在运行测试套件时执行了多少行代码。
理论上,你可以看到这可能意味着可以以科学的方式发现缺失的测试。如果我们看到一行代码没有运行,我们一定在某个地方缺少测试。这是真实且有用的,但反之则不然。假设我们在测试运行期间获得了 100%
的代码覆盖率。这是否意味着软件现在已经完全测试并正确?不是的。
考虑为一个 if (x < 2)
语句编写一个测试。我们可以编写一个测试,使这行代码执行并包含在代码覆盖率报告中。然而,一个测试不足以覆盖所有可能的行为。条件语句可能有错误的运算符——小于而不是小于或等于。它可能有错误的限制值 2,而应该是 20。任何单个测试都无法完全探索该语句中的行为组合。我们可以通过代码覆盖率告诉我们该行代码已经运行并且我们的单个测试通过了,但我们仍然可能有一些逻辑错误。我们可以有 100%
的代码覆盖率,但仍然有缺失的测试。
TODO 使用chatgpt
编写错误的测试
让我分享一个关于我的 TDD 尝试如何彻底失败的个人故事。在一个计算个人税务报告的移动应用程序中,有一个特定的 是/否 复选框,用于指示用户是否有学生贷款,因为这会影响他们应付的税额。这个复选框在我们应用程序中有六个后果,我彻底按照 TDD 的方式对每个后果进行了测试,仔细地写好了测试。
遗憾的是,我误解了用户故事。我把每个测试的逻辑完全颠倒了。复选框本应应用相关税费的地方,结果却没有应用,反之亦然。
幸运的是,这个问题被我们的 QA 工程师发现了。她唯一的评论是,她在系统中找不到任何方法来绕过这个缺陷。我们得出结论,TDD 在确保代码做到了我想让它做的事情方面非常成功,但我在弄清楚应该让它做什么方面则做得不那么成功。至少,这个问题很快就修复并重新测试了。