发布于 2026-01-06 9 阅读
0

你只应该编写有用的测试 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

你应该只编写有用的测试。

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

在我的软件开发生涯中,我接触过各种各样的代码测试态度和观点。其中两种极端观点分别是“测试不值得写,因为某些东西太复杂了”,以及“每一段提交的代码都应该附带测试”。在这两种截然相反的观点中,后一种观点虽然并非总是如此极端,但却更为普遍。本文将论证我们并非总是需要测试代码的三种情况:独立代码片段的正确性显而易见;重构时耦合不良的测试可能出现冗余;以及业务关键代码通常不可变。相反,我认为我们应该在编写任何测试之前,仔细考虑哪些地方真正需要测试。

你的考试成绩很差,你应该感到难过。

显而易见的#

如果你曾经看过单元测试相关的教程、课程或书籍,你可能见过类似以下的代码测试示例:

func Sum(x int, y int) int { return x + y;}
Enter fullscreen mode Exit fullscreen mode

毫无疑问,接下来你会得到具体的指导,学习如何编写测试来检查各种输入,以确保该Sum函数在你能够想到的每一种可能情况下都能产生正确的结果。

然而,这些教程都忽略了一个共同的问题:这个函数本身是否需要测试?看看上面的例子,你觉得它有可能没有实现它声称的功能吗?它能用更简洁的方式表达吗?它容易让人理解吗?这三个问题的答案(希望如此)都是否定的。这说明代码可以一眼就让人觉得正确,而无需大量的证明或测试。极具影响力的计算机科学家托尼·霍尔爵士曾说过一句名言:

“编写软件有两种方法:一种是使其非常简单,以至于明显不会出现错误;另一种是使其非常复杂,以至于不会出现明显的错误。”

这段话与我们之前对示例提出的问题完美契合Sum。实际上,我们可以看到,只有当某些东西“复杂到没有明显的错误”时,测试才真正必要。这些测试的价值在于证明这些不易察觉的错误并不存在。那么,对于简单且“显而易见”正确的代码,是否有必要添加测试呢?相反,在添加测试之前,你应该问自己:“这段代码是否显而易见正确,或者我能否修改它使其显而易见正确?”。如果答案是肯定的,那么对于显而易见的东西,就没有必要进行测试。

情侣#

在决定为系统编写何种级别的测试(单元测试/服务测试/UI测试/集成测试/端到端测试,或其他各种名称)时,“测试金字塔”的概念会立刻浮现在脑海中。如果您之前没有接触过这个概念,它建议我们首先在单个“单元”级别进行大部分测试。单元测试运行速度快,能够快速、经济高效地提供高代码覆盖率。然后,我们应该以更为精简的方式编写更高级别的测试,依靠这些测试来有效地验证所有组件是否已正确连接并正常通信,而不是仅仅检查逻辑中的各个分支。

测试金字塔

这套系统简单明了,乍一看似乎完全合理,而且也是普遍接受的做法。然而,它忽略了代码的可移除性或重构能力在编写测试以及编写方式上的重大考量。任何持续运行的系统都会经历单元(或独立的代码片段)的出现、消失,并随着时间的推移而演变成完全不同的形式。这是运行中的、鲜活的软件的自然发展和演进。为了强调这一点,我想问:“你是否曾经重构过代码库的某个部分,结果发现现有的单元测试完全失效或变得多余?” 如果是这样,那就说明最初的测试与代码的布局和结构过度耦合了。记住,测试本质上就是与你刚刚编写的初始代码相一致的代码(或者,如果你采用的是测试驱动开发(TDD),那么测试本质上就是与你即将编写的代码相一致的代码)。

在代码结构快速且不断变化的区域,高层测试能够提供更高的可维护性和稳定性,因为系统的高层运行机制通常更加稳定。这些测试完全失效的可能性也显著降低。

泰坦尼克号沉没

然而,这引出了一个有趣的难题:我们如何预知代码在未来何时可能在结构或方法上发生变化?如果我们能够提前识别这些变化领域,那么我们这种新获得的预知能力就意味着我们可以在第一次编写代码时就将其优化得更好。然而,遗憾的是,我们只能在黑暗中摸索:在现有知识水平下,组织代码只能是一种“尽力而为”的方法。

然而,系统存在的时间越长,或者我们投入开发的时间越长,我们对它的理解就越深入。这使得我们能够做出更明智的测试决策。对于年轻的系统或不确定性较高的系统,高层次的“黑盒”式测试最为适用,因为这些系统最有可能随着时间的推移发生结构性变化。这类测试出现冗余的可能性要小得多。相比之下,对于更成熟、更稳定或更易于理解的系统,单元测试所提供的灵活性和高效覆盖率则更为有利。

总的来说,系统的年龄、稳定性和不确定性需要作为我们编写测试的基础:测试金字塔虽然提供了一个过于简化的视角,但仍然是一个有用的参考工具。然而,我们还需要结合对代码及其随时间演变的理解来补充这一点,并提出“这些测试还能有效多久?”或者“这些测试在几个月/几年后是否可能失效?”这样的问题。

不动的#

在我参与过的许多大型软件项目中,都存在一个颇为有趣的悖论:最重要、对业务至关重要的代码往往测试最不充分。它们的输出缺乏清晰的定义,任何微小的改动都可能酿成灾难。然而,它们却依然如此。

不可移动的岩石

几年前,我参与了一个英国国家医疗服务体系(NHS)的项目。简单来说,这是一个极其复杂且基础的系统,负责将价格与医院治疗项目关联起来,并根据这些价格生成报告。报告系统经过了充分的测试,数千个测试用例细致地检查了各种输入条件下所有可能的输出结果。尽管如此,项目的核心——定价系统——却几乎完全缺乏测试。它只是在测试报告时顺带被测试了一下。代码极其难以维护,也不适合测试,因此从未进行过测试。当时我不明白,作为系统如此基础的一部分,为什么会这样处理。

后来我意识到,背后的逻辑其实非常简单。最初的代码只是概念验证,它运行正常,因此就成了生产代码。没人愿意做任何改动,因为害怕引入未知的回归问题,而这些问题一旦出现,追踪和修复起来可能极其困难且成本高昂。同样,定价流程也是一套固定的逻辑:它不会随着时间推移而改变,新的需求也不会改变它的运行方式,而且内部人员也不需要了解它的工作原理——只需要知道它能正常工作就行了。即使对于如此重要​​的代码,不做任何测试的成本也远远低于修改代码使其可测试的风险以及测试所需的工作量。

我是在提倡不对关键业务系统进行测试吗?当然不是!然而,我们必须认识到,我们生活在一个并不完美的世界。缺少关键部分测试的系统比比皆是,而且比我愿意承认的要普遍得多。但是,这并非我年轻时想象的那般灾难性。如果一段代码很复杂,但它运行正常且从未更改,那么它的测试不足又有什么关系呢?当然,在进行更改时添加测试仍然是明智之举——但我们仍然可以问自己:“测试这段代码的好处是否大于添加测试的难度?”这是一个很危险的问题,答案几乎总是“是的——添加测试”。但或许,有时,这确实值得我们认真考虑。

总结#

创建设计精良、能在项目生命周期内持续发挥作用的测试套件并非易事。“测试金字塔”方法的倡导者们过于简化了这个问题。尽管其初衷良好,但却未能扎根于瞬息万变的软件开发实际环境中:代码的演进很容易使某些测试变得冗余或不必要,有时这些测试甚至会成为重构的障碍。简洁代码的“显而易见性”也降低了对测试作为正确行为证明的需求。同样,对于已知正确且保持不变或极少更改的现有代码,也应进行简单的成本效益分析。并非所有测试都值得编写。并非所有内容都需要测试,这完全可以接受。

文章来源:https://dev.to/dglsparsons/you-should-only-write-useful-tests-3b45