我们总是可以稍后再测试,对吧?

一种替代先编写测试的方法是先编写代码,然后再编写测试。这一部分将比较和对比在编写代码之后编写测试与在编写代码之前编写测试。

一种编写测试的方法涉及编写大块代码,然后为这些代码添加测试。这是一种在商业编程中使用的方法,其工作流程可以如下所示:

image 2025 01 12 18 34 52 908
Figure 1. Figure 12.3 – Test-after workflow

在选择要开发的用户故事后,编写一个或多个生产代码片段。然后编写测试!

学术研究似乎对测试后与测试前是否有差异持不同意见。根据 ACM 2014 年的一项研究,结论的摘录如下:

“……静态代码分析结果在统计学上显著支持 TDD。此外,调查结果显示,实验中大多数开发者更喜欢 TLD(测试后开发)而不是 TDD(测试驱动开发),因为所需的学习曲线较低。”

然而,一位评论者指出,在这项研究中,以下情况适用:

“……仅从31名开发者中的13名获得了可用数据。这意味着统计分析是使用七人(TDD)和六人(TLD)组进行的。实验被发现缺乏统计效力且结果不确定,这并不令人意外。”

其他研究论文似乎也显示出类似的不尽如人意的结果。那么实际上,我们应该从中得出什么结论呢?让我们考虑一下测试后开发的一些实际细节。

后测试对于 TDD 初学者更容易

研究的一个发现是,TDD 的初学者发现测试后开发更容易上手。这似乎是合理的。在我们尝试 TDD 之前,我们可能认为编码和测试是不同的活动。我们根据一些启发式方法编写代码,然后我们弄清楚如何测试该代码。采用测试后方法意味着编码阶段基本上不受测试需求的影响。我们可以像往常一样继续编码。不必考虑测试对代码设计的影响。这种看似优势是短暂的,因为我们发现需要为测试添加访问点,但我们至少可以轻松上手。

如果我们保持与生产代码同步编写测试,稍后添加测试效果相当好:编写一点代码,然后为该代码编写一些测试——但没有为每个代码路径编写测试仍然是一个风险。

后测试使得测试每个代码路径变得更加困难

反对使用测试后方法的一个合理论点是,它变得更难跟踪我们需要的所有测试。从表面上看,这种说法不能完全正确。我们总能找到某种方法来跟踪我们需要的测试。测试就是测试,无论何时编写。

问题在于添加测试之间的时间增加。我们正在添加更多代码,这意味着在整个代码中添加更多执行路径。例如,我们编写的每个 if 语句代表两条执行路径。理想情况下,我们代码中的每条执行路径都应该有一个测试。我们添加的每条未经测试的执行路径都会使我们低于这个理想数字。这在流程图中直接体现:

image 2025 01 12 18 36 20 047
Figure 2. Figure 12.4 – Illustrating execution paths

这个流程图描绘了一个具有嵌套决策点的过程——菱形——这导致了三个可能的执行路径,标记为 A、B 和 C。执行路径数量的技术度量称为圈复杂度。复杂度分数是根据代码中存在多少线性独立执行路径计算得出的数字。流程图中的代码的圈复杂度为三。

随着我们增加代码的圈复杂度,我们增加了认知负荷,需要记住所有那些我们稍后需要编写的测试。在某些时候,我们甚至可能会发现自己定期停止编码并写下稍后要添加的测试的笔记。这听起来比我们在编写代码时简单地编写测试更为繁琐。

使用测试优先开发时,可以避免跟踪我们尚未编写的测试的问题。

后测试使得影响软件设计变得更加困难

测试优先开发的一个好处是反馈循环非常短。我们编写一个测试,然后完成少量生产代码。然后我们根据需要重构。这从瀑布式的预先设计转向了涌现式设计。我们随着逐步解决更多问题而改变设计,以响应我们对所解决问题的更多了解。

当在一大块代码已经编写之后编写测试时,整合反馈变得更加困难。我们可能会发现我们创建的代码难以集成到代码库的其余部分。也许这段代码由于接口不清晰而难以使用。鉴于我们在创建混乱代码上花费的所有努力,可能会倾向于忍受这种尴尬的设计及其同样尴尬的测试代码。

后测试可能永远不会发生

开发往往是一项繁忙的活动,尤其是在涉及截止日期时。时间压力可能意味着我们原本希望编写测试的时间根本不会到来。项目经理通常对新功能比测试更感兴趣。这似乎是一种虚假的经济——因为用户只关心有效的功能——但这是开发者有时面临的压力。

这一部分表明,如果在编写代码后不久编写测试,并且小心谨慎,那么它与先编写测试一样有效。对于刚开始 TDD 之旅的开发者来说,这似乎也更可取——但完全不测试代码的极端做法呢?让我们快速回顾一下这种方法的后果。