TDD 不适合我

2025-06-07

TDD 不适合我

本文最初发布在calhoun.io上,我在那里撰写有关 Go、Web 开发、测试等内容的文章。

这将是我第二次写关于测试的不受欢迎的观点,我相信它会引起一些人的不满。我并不总是实践测试驱动开发(也称为 TDD),而且我认为在很多情况下,实践 TDD 弊大于利。

我稍后会探讨为什么我不经常实践 TDD,但我想先声明一下,这篇文章并非抨击 TDD。事实上,我认为学习和尝试 TDD 对开发者非常有益。TDD 迫使开发者从另一个角度思考开发和设计;他们不再专注于“我该如何实现这个功能?”,而是需要退一步思考“我该如何使用这个功能?” 虽然这种改变看似微不足道,但它可以显著提升代码库的性能。

那么我为什么要写这篇文章呢?

我写这篇文章是因为很多开发者在 TDD 上苦苦挣扎,并因此感到沮丧。无论是冒名顶替综合症,还是感觉自己有不可告人的秘密需要隐藏,这些感觉在开发者身上比大多数人意识到的要多得多,因为每个人都过于担心,不愿承认自己无法让 TDD 发挥作用。当开发者听到别人谈论 TDD 如何神奇地提升了他们的效率时,情况会变得更加糟糕。简而言之,很多程序员感觉自己很糟糕,而他们不应该这样;TDD 是一个很好的学习工具,但我可以亲身体验它并不总是有效,很多情况下,它只会阻碍我快速编写高质量代码的能力。

好吧,现在让我们深入探讨一下为什么我不一直实践 TDD。

TDD 过于注重单元测试

测试驱动开发通常被描述为一个包含三个步骤的过程:红灯、绿灯、重构。

这里的想法很简单:

  • 红色步骤是您编写的测试将会失败,因为它测试了您尚未实现的内容。
  • 绿色步骤涉及编写代码以使测试通过
  • 重构步骤是将代码重构为更易于维护的代码,同时使用测试来确保不会破坏任何功能。在此步骤中,请勿添加任何新功能。

我尝试严格遵循 TDD,但有时感觉非常痛苦;它时不时地拖慢我的速度,而不是帮助我提高工作效率。

直到我读到 Jeffrey Way 的这条推特,我才真正明白为什么会出现这种情况:

我这里只放了第一条推文,不过你应该去读完整个帖子。这绝对值得你花时间。

Jeffrey 说出了我长期以来一直在努力解决的问题;当我们过于关注单元测试而完全忽略每个开发人员迟早都会遇到的更复杂的场景时,TDD 很难学习和掌握。

几乎在每一个我发现使用 TDD 很麻烦的情况下,这几乎总是源于我试图对一些需要模拟一切的代码进行单元测试。

TDD 的初衷是帮助我们从代码实现中抽离出来,专注于代码的最终用途。但当我编写单元测试时,需要模拟大量依赖项,这显然行不通。我不得不再次思考实现细节,例如“我的代码需要访问数据库吗?”以及“编码怎么办?我们需要支持多种输出格式吗?我应该在测试中注入一个模拟编码器吗?”

我觉得这样思考代码很不自然。在开始使用依赖项之前就开始思考需要哪些依赖项毫无益处。相反,我退一步思考,编写一个功能测试,这样工作起来会更好。比如,“当我POST访问/orders包含 JSON 的路径时{"price": 123, ...},我希望返回以下 JSON,并且其 ID 与ord_[a-zA-Z0-9]{8,}$模式匹配。”

这种测试不仅超级容易编写——我们只需启动一个应用程序,访问终端,然后检查结果——而且还能让我回到正确的思维模式。我现在会思考别人可能会如何实际使用这些代码;我会思考其他开发者如何与我的 API 交互,或者真人如何填写并提交表单。

当然也有例外。比如,如果我正在写一个函数来分解数字,TDD 就能找到一个合理的解决方案。关键在于,我们其实并没有真正关注模拟和依赖;我们只是在编写一个独立的函数。在这种情况下,TDD 就能大放异彩,前提是我们没有落入第二个陷阱,即认为我们必须始终编写尽可能少的代码。

我也不是说你不应该写模拟测试的单元测试。这些测试在很多环境下都能发挥作用。我只是发现自己在写单元测试的时候不太经常实践 TDD。相反,我会先对功能测试进行单元测试,然后再根据自己的情况编写单元测试。

并非所有代码都应该一次编写一个测试用例。

TDD 通常使用以下规则进行教学:

  1. 除非是为了通过失败的单元测试,否则您不得编写任何生产代码。
  2. 您不能编写任何足以导致失败的单元测试;并且编译失败也是失败。
  3. 您不得编写超出足以通过一个失败的单元测试的生产代码。

这些规则由罗伯特·C·马丁(鲍勃叔叔)撰写,可以在网站butunclebob.com上找到

虽然这在很多情况下都有效,但我完全不同意所有代码都应该这样编写的想法。

我期望有人能给我发一个视频、博客或其他示例链接,其中开发人员使用 TDD 来推导一些复杂的算法。一个常见的例子是编写一个函数来确定一个数的因数。我也看到过一些文章,作者探讨了是否可以通过 TDD 推导类似快速排序的算法。

当然,如果这些更复杂的算法可以通过 TDD 推导出来,那么它必须在所有情况下都有效,对吗?

虽然 TDD 有时可以用来推导合理的算法,但我也见过无数事与愿违的例子。开发人员使用 TDD 推导出来的算法极其缓慢、低效,有时甚至会遗漏一些边缘情况,因为几乎不可能考虑到所有边缘情况,更不用说测试所有边缘情况了。

事实上,有些情况下,你确实需要坐下来思考一些比函数预期输入和输出更多的事情。最容易理解的例子可能是排序算法——虽然你可能从测试驱动开发 (TDD) 方法中推导出快速排序,但你也可能很容易地推导出一个速度慢得多、效率低得多的算法。即使你真的想出了一个高效的快速排序,大多数标准库也使用比这更复杂一些的方法,因为对于较小的列表,使用快速排序效率较低。

除了算法之外,频繁切换工作环境有时也会损害整体生产力。虽然对许多开发人员来说,不断地编写一个测试,然后写一两行生产代码,再测试、重构,然后重复这些步骤可能效果很好,但我个人认为这种频繁的切换会分散注意力。我发现,当我做到以下几点时,我的效率会更高:

  1. 编写一些测试用例来演示我期望的基本功能。
  2. 花点时间思考如何实现该功能。
  3. 实现一个粗略的版本以使我的测试通过。
  4. 根据需要重构。

这与 TDD 非常相似,但并不完全相同。如果我把它作为我的 TDD 版本来教授,我敢肯定很多人会告诉我“我做错了”。🤷‍♂️

总结

我发现 TDD 有时确实有好处,我并不是说我们应该放弃它。相反,我想表达的是,被这套定义什么是 TDD、什么不是 TDD 的严格规则所束缚是一个错误。

我们应该从 TDD 中汲取经验教训,并以最有效的方式运用它们。如果这意味着我们最终会违反一些规则,那就顺其自然吧。毕竟,TDD、敏捷开发,或者任何开发流程的目标都是让我们更好地完成工作,如果它们没有做到这一点,那么就需要做出一些改变。

想学习围棋吗?

有兴趣学习或练习 Go 语言吗?不妨看看我的免费课程——Gophercises - Gophers 新手编程练习

我还有一些高级课程,涵盖使用 Go 进行 Web 开发使用 Go 进行测试,您也可以查看。

文章来源:https://dev.to/joncalhoun/tdd-is-not-for-me-4ode
PREV
像专业人士一样在应用程序之间重用 React 组件快速设置跟踪和隔离可重用组件定义零配置可重用 React 编译器版本和导出可重用组件在新应用程序中安装组件从使用应用程序中修改组件在第一个应用程序中更新更改(签出)结论
NEXT
掌握这些技巧,像专业人士一样使用 Postman 😎