“AI 技术+人才”如何成为企业增长新引擎?戳此了解>>> 了解详情
写点什么

作为现代开发的基础,为什么 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:258889

评论 1 条评论

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

科技前沿:一张图数字孪生北京大兴国际机

一只数据鲸鱼

数据可视化 智慧城市 指挥交通 智慧航空

混合推荐系统介绍(二十二)

数据与智能

推荐系统 计算

为什么很多时候,我们会感觉企业越大,效率越低呢?

石云升

职场经验 管理经验 6月日更

JavaScript中的Set数据操作:交集、差集、交集、对称差集

devpoint

set JavaScrip 6月日更

千古无同局?围棋在线教育还有这样的打开方式!

亚马逊云科技 (Amazon Web Services)

国足晋级12强!看人工智能如何更好地预测世界级赛事!

亚马逊云科技 (Amazon Web Services)

为什么要学习JVM,仅为面试?又该如何学习?

Java架构师迁哥

百度AICA迎来毕业季,55位新晋“首席AI架构师”推进产业智能化

百度大脑

人工智能 百度 架构师

北鲲云超算平台如何加速生命科学研究

北鲲云

【视频】51CTO专访博睿数据COO吴静涛,解读IT运维“新范式”

博睿数据

Flutter 改善套娃地狱问题(仿喜马拉雅PC页面举例)

小呆呆666

flutter android 大前端 iOS Developer

DNS劫持该如何处理

网络安全学海

程序员 运维 网络安全 信息安全 DNS

一妹子揭露美团面试中一些不愉快的事情(Java岗)

Java架构师迁哥

密码合规测评新服务:“微咨询”正式发布

腾讯安全云鼎实验室

密码合规 微咨询

【源码篇】Handler那些事(万字图文)

小呆呆666

Java android 源码 Android Studio Android进阶

5分钟速读之Rust权威指南(三十二)互斥体

wzx

rust

快手封停多个内容侵权账号:如何严打短视频内容侵权行为

石头IT视角

网络攻防学习笔记 Day59

穿过生命散发芬芳

网络攻防 6月日更

Flutter GetX使用---简洁的魅力!

小呆呆666

flutter android 大前端 iOS Developer

新常态下的CMDB系统规划与落地

云智慧AIOps社区

CMDB 智能运维

Dubbo 3.0.0 来了!还学得动吗?

青年IT男

dubbo

《原则》(二十九)

Changing Lin

如何优雅的设计DWS层?

云祁

大数据 数据仓库 维度建模

一种更优雅的Flutter Dialog解决方案

小呆呆666

flutter android 大前端 iOS Developer

解放生产力,自动化生成Vue组件文档

vivo互联网技术

Vue 自动化 大前端 组件

如何设计恒流源输出电路?

不脱发的程序猿

嵌入式 电路设计 硬件研发 恒流源输出电路

eKuiper 与百度智能边缘框架 BIE 集成方案

EMQ映云科技

边缘计算 边缘技术 边缘流式数据 #百度# 智能IoT边缘服务

【源码篇】ThreadLocal的奇思妙想(万字图文)

小呆呆666

Java 源码 架构 源码分析 设计

为你的身份证“加盾”,鉴伪更精准,传输更安全

百度大脑

人工智能 数据安全 身份证

高性能计算与人工智能何处去?英特尔剑指XPU

E科讯

前端 JavaScript 中 JSON.stringify() 的基本用法

编程三昧

JavaScript 大前端

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