最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

作为现代开发的基础,为什么 TDD 没有被广泛采用?

  • 2022-09-07
    北京
  • 本文字数:4813 字

    阅读完需:约 16 分钟

作为现代开发的基础,为什么TDD没有被广泛采用?

测试驱动开发在 1999 年左右是最前沿的技术,也是现代开发的基础,但为什么直到现在还没有被广泛使用?

 

“我认为,在我作为一名专业极客的四十二年生涯中,软件行业在历史上始终不能或不愿意掌握和采用测试驱动开发(TDD),这是最令人沮丧和丧气的事件之一。”对于 TDD 没有广泛被应用的问题,GeePaw Hill 发了系列推文进行了探讨。他认为问题在于其支持者在组织方面的失败,他们推动得太猛,想将“TDD”转化为“测试很好”。

 


对此,我认为:对于那些最坚定的支持者来说,其实TDD 并不像他们认为的那么有价值。

 

他们中的大多数人将 TDD 的价值基于自己的经验,因此,我也想基于我的经验来谈谈这个问题。先从我的背景开始讲起吧。我将自己视为“TDD 人”。早在 2012 年我就学会了 TDD,它帮助我获得了第一份软件工作,而我之前的两份工作,都是在 Ruby 中严格执行 TDD。有那么一段时间,我所有的个人项目都遵循严格的 TDD,如果有一天,我头脑一热,创办了一家科技创业公司,我也会使用 TDD 来开发软件。我在 2018 年的时候就会为 TDD 辩护,现在也仍然会为 TDD 辩护。

 

我和他们的区别在于,我将 TDD 视为一项“有些用处”的技术,是众多技术中的一项;而那些最强烈的倡导者则认为 TDD 是一种“变革”。有些人声称,TDD 对编程的重要性,就像洗手对医学的重要性一样

 


为什么会有区别?因为我们指的是两件不同的事情。我实行的是“弱 TDD”,这只是意味着“在代码之前编写测试,在短的反馈周期内”。这有时被贬低为“测试优先”。而强 TDD 遵循的是一个更严格的“红-绿-重构”周期。

 

  • 编写一个最小的失败测试。

  • 编写尽可能少的代码来通过测试。

  • 在不引入新行为的情况下重构一切。

 

重点是极简(minimality)。在其最纯粹的形式中,我们有 Kent Beck 的 test && commit || reset (TCR):如果最小的代码没有通过,那么就把所有的修改都删除,然后重新开始。

 

另外,对于为什么要进行 TDD,我们也有不同的看法。强 TDD 的支持者们常常声称,这并非一项测试技术,而是一种偶然使用测试的“设计技术”。但我对这一说法感到困惑,原因有二。首先,他们使用“设计”的方式,和我有很大的区别:本地代码组织与系统规范。其次,很多人说它一直就是这样的,而原书中明确地声称,它是一种测试技术。不管怎么说,这是现代强 TDD 的一个核心原则:TDD 让你的设计变得更好。换句话说,弱 TDD 是一种技术,而强 TDD 则是一种范式。

 

没意义的极致主义

 

没有人愿意听别人说他们做错了,尤其是他们做错的时候。

 

如果你尝试了 TDD,但它没有“起效”,而实际上你所尝试的东西根本不是 TDD,那又会如何?

——反对 TDDAgainst TDD

 

为了避免每一个细微的差别,我将集中讨论 TDD 的“极繁主义(Maximalism)”模型:

 

  • 除最特别的情况外,在任何情况下都必须使用 TDD。

  • 应该尽可能严格遵循 TDD 周期(尽管 TCR 是不必要的)。

  • 测试优先并非 TDD。

  • TDD 总是能带来更好的设计。

  • TDD 可避免其他形式的设计。

  • TDD 可避免其他形式的验证。

  • TDD 不会失败。如果它引起问题,那是因为你做错了。

 


TDD 和生产力之间的权衡关系到学习曲线。一旦你到达山顶,那就没有什么权衡的事了。如果你还在谈论权衡,那就表明你可能在山上的什么位置。

 

我认为,真正的极致主义者并不多,尽管我至少遇到过一个。大多数倡导者在某些方面是温和的,但在另一些方面却是偏激的——我当然也不例外!但是对于更广泛的 TDD 对话是什么样子的,极致主义者是一个很好的模型。尽管人们只是在口头上谈论诸如“使用合适的工具”“没有银弹”之类的东西,但是他们经常发表他们的极致主义的观点,而不分享他们的注意事项。极致主义思想,在整个学科中得到了广泛的传播。

 

极致主义分析

 

TDD 的极致主义案例来自两个方面:它对你的测试和设计都有好处。

 

验证

 


TDD 的开发是复式簿记,同样的原则,同样的推理,同样的结果。

 

这条推文的论点很简单:在极致的 TDD 下,所写的每一行代码都会被测试所覆盖,这样就会发现更多的 bug。我对此深信不疑。测试覆盖率越高,意味着 bug 越少。

 

问题在于,TDD 测试非常受限制。为了使 TDD 周期保持快速,你的测试需要快速编写和运行,而且要能在“一秒之内完成数百次的测试”。唯一符合这三个标准的测试是手工制作的单元测试。这就将其他形式的测试排除在外:集成测试、端到端测试、突变测试、模糊测试、性能测试、基于模型的测试。

 

要想让单元测试足够充分,就必须替代所有其他形式的测试。还必须替代基于非测试的验证技术:手动测试、代码检查、类型系统、静态分析、合同、把断言语句推得到处都是。

 

“可是,从来没有人说过,你只需要做一个单元测试!”好吧,我们认为自己很幸运,因为我曾经多次经历过这种极繁的情形:如果你使用 TDD,你将不存在任何 bug,因此,如果你存在 bug,那就是你的 TDD 使用不当。

 

TDD 是一种“设计”方法吗?

 


测试驱动开发(TDD)并非一种测试方法。它是一种设计方法。通过使用自动测试,它可以帮助你构建干净、经过测试和无错误的代码。测试不是 TDD 的输出。测试是输入,干净的设计和代码是输出。

 

就像我以前说过的,TDD 的倡导者使用“设计”的方式与我截然不同,所以让我们先解释一下其中的区别。

 

对于我而言,设计就是软件的规范。我们需要处理一个问题,以及我们希望保留的一些属性,我们的系统能够满足这些要求吗?比如,设想一个工作器,可以从三条数据流中提取数据,把这些数据合并在一起,然后把他们上载到数据库。我要保证不会出现重度的数据,流的停顿能够得到优雅地处理,所有的数据最终都会合并,诸如此类。我不在乎代码为“API 请求”调用了哪些方法,也不在乎 JSON 响应是怎样转化为域对象的。我只在乎它对数据做了什么。

 

与此相反,“设计”在 TDD 中是怎样组织代码的。munge 是一个公共的还是私有的方法?我们是否应该把 http 响应处理程序分割成独立的对象?check_available 方法的参数是什么?TDD 的倡导者们谈到了“倾听你的测试”:如果编写测试很困难,那就说明你的代码有问题。你应该重构代码,使其更容易测试。换句话说,难以通过 TDD 进行测试的代码组织得很糟糕。

 


TDD 是一种设计技术。如果你不需要设计,那么你就不需要 TDD。(测试只是设计过程的一个很好的副作用。)我简直无法想象这样的系统是如此地小,以至于可以不需要任何设计。

 

但是 TDD 是否能确保良好的组织?我并不这么认为。我们知道,TDD 的代码看上去是不同的。在其他方面:

 

  • 依赖注入。这使得代码更容易配置,但代价是使其更加复杂。

  • 大量的小函数而不是几个大函数。

  • 广泛采用公共方法,而非深入使用私有方法。

 

这些一定是坏事吗?不是的,它们会把事情搞砸吗?是的。有时候,大的函数会带来更好的抽象,而小的函数会导致混乱的行为图。有时候,依赖注入会使代码变得更加复杂,难以理解。有时候,大型公共 API 会让模块之间的耦合变得更紧密,这就是为了鼓励重用“实现对象”。如果 TDD 与你的组织相抵触,那么有时 TDD 是错误的。

 

现在,这是一个相当弱的论点,因为它同样适用于任何种类的设计压力。极繁主义更具体的问题是,代码组织必须以极少的步骤开发。这导致了路径依赖:代码的最终结果会受到你所采取的路径的强烈影响。按照极繁的 TDD,下面是我写的前七个测试:

 

quicksort([]) # prove it existsassert quicksort([]) == []assert quicksort([1]) == [1]assert quicksort([2, 1]) == [1, 2]assert quicksort([1, 2]) == [1, 2]assert quicksort([1, 2, 1]) == [1, 1, 2]assert quicksort([2, 3, 1]) == [1, 2, 3]
复制代码

 

下面是传递它的最小代码:

 

def quicksort(l):    if not l:        return []    out = [l[0]]    for i in l[1:]:        if i <= out[0]:            out = [i] + out        else:            out.append(i)    return out
复制代码

 

需要澄清的是,我并不是想在这里表现得反常,当我严格遵守 TDD 时,我就是这样做的。有了更多的测试,它就会趋于正确,但由于我们将代码封装在一组小型的测试中,因此设计将会变得很不可靠。

 

既然我说我正在做的是“弱 TDD”,所以我还是会在快速排序(QuickSort)之前写一个测试。但与最大的 TDD 不同,我不会去编写一个单元测试。而是像下面这样编写:

 

from hypothesis import givenfrom hypothesis.core import exampleimport hypothesis.strategies as st

@given(st.lists(st.integers()))def test_it_sorts(l): out = quicksort(l) for i in range(1, len(l)): assert out[i] >= out[i-1]
复制代码

 

这是一个属性测试的示例。我不是对一堆具体的示例进行编码,而是按照排序的定义进行编码,测试将在随机列表上运行我的代码并检查属性是否成立。概念上的统一进一步深化,这也推动了更好的组织。

 

这导致了我对极繁主义的 TDD 最大的不满:它强调局部组织而不是全局组织。如果它能让你不对一个函数进行整体思考,那么它也能让你不对整个组件或组件之间的交互进行整体思考。它能带来更好的设计。

 

James Shore 发了推文

 

架构对于前期设计来说太重要了。

 

(事实上,我最痛恨的就是这会让人混淆代码组织和软件设计,而非 TDD 的人也会将这两者混淆,因此,或许我只会选择一个非常差劲的话题来进行宣传。)

 

弱 TDD 的好处

 

我已经讲了很多关于 TDD 的废话。就像我以前说过的,我常常实践 TDD 的“弱”形式:在编写代码之前先编写一些验证,但又不坚持极致,甚至不坚持基于测试的验证。TDD 的极繁主义者也许会说它并非“真正的 TDD”,让他们见鬼去吧。

 

弱 TDD 有四个好处:

 

  1. 你可以编写更多的测试。如果编写一个测试“Gates”来编写代码,你就必须这么做。如果你可以以后再编写测试,你就可以一直拖着,而且永远不会去编写。在我看来,这是向早期程序员教授 TDD 的主要好处。

  2. 重构更容易,因为你更容易抓住回归的问题。

  3. 现在,在开发代码时,所有代码都至少有一个客户端。这会告诉你界面是否太过笨拙。

  4. 它会让你养成一种习惯,就是在你实际没有使用单元测试的情况下,也要考虑你的代码如何被验证。

 

等等,这些不就是和极繁的 TDD 一样的好处吗?“它检查你是否有笨拙的界面”听起来非常像“倾听你的测试”。嗯,是的。你应该倾听你的测试!TDD 经常使你的设计变得更加完美!

 

我的观点是,它也可能使你的设计变得更糟。有 TDD 比没有 TDD 好,但没有 TDD 比过度的 TDD 好。TDD 是一种你与其他方法结合使用的方法。有时你会听从这些方法,他们会给出相互矛盾的建议。有时,TDD 的建议会是正确的,有时会是错误的。有时它会错得离谱,以至于你在那种情况下不应该使用 TDD。

 

为什么 TDD 还没有征服世界

 


今天真是大开眼界。测试驱动开发在 1999 年左右是最前沿的。它是现代开发的基础。我无法想象不使用它。听到公司不使用它,就像听到公司说“你听说过这个叫 Linux 的新东西吗?”卧槽。

 

所以,在所有这些之后,我有了我的假设,即为什么 TDD 没有传播开来。老实说,这是一种相当反常的假设。极繁的 TDD 并不像极繁主义者所认为的那么重要。TDD 在方法组合中使用得更好。因为有用的方法远远多于一个人所能掌握的,因此,你要选择你想擅长的。通常情况下,TDD 不会被选中。

 

我将其与 Shell 脚本相提并论。今年这个春季,我花费了大量的时间来学习 Shell 脚本。我想每位开发者都应该懂得怎样编写自定义函数。这是否比 TDD 更重要呢?如果人们没有时间去同时学习,他们会选择哪个呢?如果使用合适的 TDD 所花的时间太长了,那么你能在 Shell 脚本和调试实践中学到一些东西吗?人们什么时候才能停下来?

 

结语

 

我甚至不知道我的结局是什么。写这篇花了我三天时间,我不知道它是否让我或你们中的任何一个人有了更清晰的认识。我甚至不知道我的理解是否正确,因为我并没有做很多研究,也没有处理过一些细节上的问题。

 

原文链接:

 

https://buttondown.email/hillelwayne/archive/i-have-complicated-feelings-about-tdd-8403

2022-09-07 20:258911

评论 1 条评论

发布
用户头像
TDD 是刚需,好吗?即使你不是写框架,就算写业务,也要 TDD
2022-09-14 18:47 · 广东
回复
没有更多了
发现更多内容

用uniapp写一个内外循环的全选与反选,不会的赶紧围观

CRMEB

省掉80%配置时间,这款Mock神器免费又好用

Liam

前端 前端开发 Postman 前端教程 web前端开发

恒源云(Gpushare)_自动化训练小技巧白送给你,不要吗?

恒源云

OSS SSH hy-tmp

hash,bloomfilter,分布式一致性hash

Linux服务器开发

分布式 hash 后端开发 Linux服务器开发 C++后台开发

科创中国开源创新榜单发布,EMQX 获评“年度优秀开源产品”

EMQ映云科技

开源 物联网 IoT emq emqx

如何优雅的记录操作日志

flyhero

Java Spring Boot 后端 造轮子 4月月更

去中心化的 React Native 架构探索

Shopee技术团队

前端 去中心化 React Native

借品牌升级之际,谈一谈技术开发者为什么选择 InfoQ 写作社区

宇宙之一粟

4月月更 InfoQ写作社区2周年

从趋势到必选项,探讨企业数字化转型方式方法

华为云开发者联盟

数据 数字化 企业数字化转型 业务数字化

【分享汇总】AIoT开源科技节暨OpenHarmony技术论坛(附链接)

OpenHarmony开发者

OpenHarmony AIoT开源科技节

【高并发】一文秒懂Happens-Before原则

冰河

并发编程 多线程 协程 异步编程 精通高并发系列

记一次CPU持续增长的问题解决

BUG侦探

Python py-spy CPU增长问题

STI即将登录Gate.io,我们有哪些期待?

小哈区块

VNC中文是什么意思?全称是什么?

行云管家

运维 服务器 vnc

公司产品手册的编写方法

小炮

企业 产品宣传手册

48天打造你的专属 Twilio——浅谈运营商通信中台

网易云信

通信

踩了个DNS解析的坑,但我还是没想通

捉虫大师

DNS 问题排查 4月月更

亚马逊云科技 loT 百亿连接力量

亚马逊云科技 (Amazon Web Services)

亚马逊云

腾讯二面:Linux操作系统里一个进程最多可以创建多少个线程?

Java全栈架构师

Linux 程序员 架构 面试 操作系统

大数据培训Hive如何控制map个数与性能调优参数

@零度

hive map 大数据开发

看板的作用是什么?任务看板如何跟进

阿里云云效

云计算 阿里云 持续交付 看板 项目协作

STI生态迎来新进展,登录Gate.io意味着什么?

西柚子

问题来了!拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?

Java全栈架构师

程序员 架构 面试 计算机网络 底层知识

百度程序员开发避坑指南(3)

百度Geek说

前端

详解离线数仓和实时数仓的区别

五分钟学大数据

4月月更

Sitemap的重要性

源字节1号

软件开发 网站优化

进阶篇|有了这招,用文本编辑器搞前端代码都能保证格式统一

Jianmu

运维 前端 自动化 工作流 格式化

一张长图带你看懂物联网产业十数载“江湖风云”!

亚马逊云科技 (Amazon Web Services)

物联网

初创企业需要CRM系统的原因

低代码小观

初创公司 企业管理系统 CRM系统 客户关系管理系统 初创型企业

多个私有云设施管理用什么云管理软件好?

行云管家

云计算 私有云 云管理 多有云

百度程序员开发避坑指南(移动端篇)

百度Geek说

移动端

作为现代开发的基础,为什么TDD没有被广泛采用?_文化 & 方法_Buttondown_InfoQ精选文章