跳转至

08 TDD中的驱动(1):驱动的极限是什么?

你好,我是徐昊。从今天开始,我们来讨论一下测试驱动开发中的驱动是什么意思。

在上节课,我们讨论了测试驱动中测试的性质。它们不是“单元测试”,而是不同粒度的功能测试。或者如我建议的那样,你可以叫它们“单元级别功能测试”。

那么这些单元级别功能测试将会如何驱动我们的开发呢?今天我们就来讨论一下这个问题。

如果当初我们那么做了

上节课我们讲到,TDD中测试针对的粒度是独立的功能上下文或变化点。测试验证功能上下文或变化点符合功能需求。

对于相同的功能,如果我们划分的功能上下文不同,会有什么结果呢?回看之前的TDD演示,在第一讲中,我们讨论了三种实现方式:

  1. 从给定的参数列表中寻找对应选项,并根据选项类型读取参数;
  2. 将参数列表按照选项,分割成由选项名称和参数组成的数组;
  3. 将参数列表按照选项,分解成由选项名称和参数组成的映射。

这三种选择实际上就划分了不同的功能上下文。当时出于实现方式复杂度的考虑,我选择了第一种实现方式。即,从功能上下文的角度考虑,也就是从API视角出发,将全部功能当作一个上下文。那么如果我们当时选择了其他的实现方式,会如何呢?请看接下来的视频演示。

好,到此为止,通过红/绿循环,我们实现了参数分割的功能。剩下的部分,其实与我们之前展示的相差不多。我稍微演示一两个测试,其他的你可以自行操作。

可以发现,对比第一次我们做的实现,不同的实现策略,隐含着不同的功能上下文划分。针对不同的功能上下文,我们编写对应的单元级别功能测试来验证其功能。在这些单元级别功能测试的指引下,我们逐步完成了软件的功能。也就是说,功能上下文的划分,指引我们编写测试;在测试的驱动下,我们逐步完成功能上下文的实现。

从这里,我们似乎可以窥探到测试驱动开发的核心要点:单元级别功能测试能够驱动其对应单元(功能上下文或变化点)的外在功能需求。而对于对应单元之内功能的实现,测试就没有办法了。

以上面演示的视频为例,我们需要将参数列表进行分解。但参数列表的分解是Args.parse内部实现的方式。当我们从功能测试的角度测试Args.parse时,是无法得知Args.parse是如何处理参数列表的。所以,无论是这节课的实现方式,还是第一讲里的实现方式。从Args.parse这个功能上下文来看,测试是极端类似的。

如果我们要驱动单元内的功能实现,该怎么办呢?那么就需要将这个单元对应的功能上下文,分解为更小的上下文,并将功能需求在这个上下文中加以分解。如下图所示:


比如上面演示的视频中,Args.parse是一个大的功能上下文。按照我们的实现思路,我们将它分解成了一个小的功能上下文:将参数列表分解为映射。那么我们可以将参数列表分解放入另外一个单元(Args.toMap),对它进行测试,从而驱动它的实现。

也就是说,单元级别功能测试无法驱动小于其测试单元的功能需求,也无法驱动单元内的实现方式,需要进一步拆分功能上下文才可以。而指引功能上下文拆分的方式有很多,比如有不同的实现思路、架构等。

TDD的极限

曾经有些人希望通过构造难以用测试驱动出实现的需求,来证明TDD不是一种有效的开发方法。对于这些人,我都懒得回应。

第一,是因为这样的需求一点儿都不难构造;第二,TDD并没有宣称它是所有开发问题的答案,所以找到一个反例又能说明什么呢?第三,TDD的效用在于将研发过程工程化。

那么无法通过TDD有效实现需求,也只意味着这类问题不能有效工程化而已。排除一个错误答案,并不代表能找到正确的答案。

根据我们上一节的讨论,让TDD丧失驱动力最简单的办法,就是指明某个单元内的实现细节。比如,使用冒泡法对数组进行排序。因为从功能角度来说,冒泡法还是快排序,是没有差别的:

@Test
public void should_sort_by_bubble_sort() {
  assertArrayEquals(new int[] {1, 2, 3}, bubbleSort(3, 1, 2));
}

@Test
public void should_sort_by_bubble_sort() {
  assertArrayEquals(new int[] {1, 2, 3}, quickSort(3, 1, 2));
}

如果我们需要在测试中体现不同排序算法的差异,以驱动不同的实现,那么就需要改用行为验证。请观看接下来的视频演示:

至此,我们可以明白,测试驱动开发的主要关注点在于功能在单元(模块)间的分配,而对于模块内怎么实现,需要你有自己的想法。当然Kent Beck说得更直接:TDD可以提高效率,但不能避免愚蠢。

如果真的不知道怎么实现

如果真的不知道该怎么实现,要怎么办呢?那么TDD仍然可以帮你提高效率。就是这么神奇!

还是以ArgsTest为例,如果我就是不知道参数分解这一步到底要怎么实现,那么我能怎么办呢?

正如视频中演示的,就算你真的不知道某个关键的功能要如何实现,只要你能列出对于这个单元的期待,那么你仍然可以完成其余功能,然后再找人帮忙!

只不过此时,你就算找人帮忙,对于这个模块的输入输出也已经了然于胸。当然,你还可以写一组测试,验证别人帮你写的代码是否真的可以完成你想要的功能。甚至你还可以带着测试代码到Stack Overflow上去求助,肯定会有意想不到的收获。

小结

这节课我们讨论了测试驱动开发到底驱动了什么:功能在单元(模块)间的分配。我们也讲了,测试驱动开发在什么地方会失去驱动力:单元(模块)内的实现方式。

那么很有意思的事就来了,从“驱动”的角度来说,TDD实际上并不是一种编码技术(Coding Technique),它无法帮助实现你不会写的代码,你必须要知道如何实现这些功能;但是一旦你明确了要实现的功能,并且知道要怎么实现,TDD可以帮助你更好地将功能放置到不同的单元。也就是说,TDD“驱动”的是架构,因而实际是一种架构技术

是的,这正是我们讲的编码架构师(Coding Architect),也是真正的实干型而非PPT型架构师(当然你可以省略地讲,这是真正的架构师)。这也是为什么TDD也被看作Test Driven Design。当然,我觉得Test Driven Development其实描述得更全面。

思考题

请从架构的角度出发,思考红/绿/重构循环,分别发挥了什么作用?

编辑来信

TDD是一项技能,唯有动手实操、反复练习,才能有所小成。为了帮助你更快地进步,徐昊老师特发起了“TDD专栏首发·代码评点”活动。

在第一个实战项目结束后,我们会根据你提交的学习反馈,手动选出其中几位进行代码评点与解疑答惑。而评点的详细内容我们也将制成加餐,展示在专栏里,供其他同学学习与参考。

划重点!如果学完第1-10讲再写反馈,将会大大提高你入选的机会!另,此次收集时间截至4月3日零点。所以非常希望你能跟上我们的更新进度,多动手实操,并记录学习体会。

最后,希望我们都能好好学习,更上层楼!

精选留言(15)
  • 邓志国 👍(28) 💬(1)

    最长碰到的挑战就是问:TDD如何驱动某个算法的实现。这篇文章给了很好的答案。

    2022-03-29

  • davix 👍(5) 💬(1)

    請問老師,我體會到的一個現實情況是實現跟設計常常是同時進行的。也就是說大多情況拿到一個需求,開始只有一個模糊的設計。對於功能模塊的劃分,各模塊對外接口具體參數等,是在實現的時候逐步明確清晰的。如果ㄧ上來對邊界就十分明確,自然很容易ㄧ個個寫成測試,再一個個實現pass就行了。但這種情況很少。您怎麼看?

    2022-04-09

  • aoe 👍(4) 💬(1)

    TDD 不能做什么? - 不能窥探方法内是如何实现的,例如对于一个排序算法,只能验证结果是否符合预期,不能验证使用了哪种算法(冒泡、插入等) - 不能求解自己不会的问题,例如: - 自己没有思路的算法题 - 破解世界七大数学难题获得高额奖金

    2022-03-30

  • Sudouble 👍(10) 💬(0)

    红:搭建一个框架; 绿:实现了框架; 重构:改进了框架。 是一个从无到有,从有到完善的架构演进过程。

    2022-04-11

  • aoe 👍(5) 💬(4)

    原来以为可以学好了 TDD 在 LeetCode 上大杀四方,看来我想多了! 多谢老师告知 TDD 不能做什么

    2022-03-29

  • Geek_a03aa5 👍(4) 💬(0)

    红:这个需求很简单,怎么实现我不管 绿:不要跟老夫说设计模式、面向对象,jQuery 一把梭.jpg 重构:改掉上面产生的坏味道 在每个阶段只需要达成一个小目标,个人感觉是用来降低开发过程中的认知负担的好办法。

    2022-06-29

  • 枫中的刀剑 👍(3) 💬(0)

    红:以代码的形式呈现功能最终的样子,以终为始的思想,通过测试迫使自己去思考功能的上下文。 绿:将想法变为现实。通过红的步骤我们可以只专注在一个小的上下文上来思考如何实现,降低认知负担。 重构: 对功能和结构再次调整,消除不合理的地方,朝更好的设计前进。

    2022-03-30

  • 范飞扬 👍(1) 💬(0)

    现在不用带着测试代码到 Stack Overflow了。带着测试代码找GPT,它会帮你实现

    2024-01-15

  • Kea 👍(1) 💬(0)

    道生一 一生二 二生三 三生万物 tdd 可以帮助你高效且工程化地由一生二 二生三 三生万物 但是不能帮你如何实现道 本质你还得知道道是什么 而且 tdd 本身也是一种道

    2022-09-05

  • Frode 👍(1) 💬(0)

    1. TDD“驱动”的是架构,因而实际是一种架构技术。 2. 测试驱动开发到底驱动了什么:功能在单元(模块)间的分配。我们也讲了,测试驱动开发在什么地方会失去驱动力:单元(模块)内的实现方式 3. 如果我们要驱动单元内的功能实现,该怎么办呢?那么就需要将这个单元对应的功能上下文,分解为更小的上下文,并将功能需求在这个上下文中加以分解。 4. 如果真的不知道怎么实现?拆分功能,可以先mock出来,然后实现可以实现的,再想办法解决,例如请教他人

    2022-03-30

  • Geek_99be51 👍(0) 💬(0)

    所以,在更细力度的测试需求的驱动下,模块的功能从耦合到松散,架构也越来越清晰。

    2024-02-04

  • buoge 👍(0) 💬(0)

    TDD 解决的是业务需求 到功能模块 该如何拆分, 功能模块是否正确工作! 从它并没有解决代码实现的细节,而是实现了一个框架,从这个角度来讲,它是一种架构技术。

    2023-04-08

  • zhuyc 👍(0) 💬(0)

    红:描述需求 绿:实现需求 重构:优化实现 设计是对需求进行分解并重组。 TDD过程: - 用验收测试用例明确需求(然后disable掉这些用例) - 根据经验分解出能实现部分(TODO list) - 红,绿循环实现部分 - 逐渐拼凑出需求,通过验收用例 - 过程中根据bad smell情况,适时重构代码/调整TODO list

    2022-11-29

  • eoeoeo 👍(0) 💬(0)

    从架构的角度来说 红-根据当前需求设计最终希望得到的目标或者结果 绿-找到一种能满足需求的架构设计或者备选架构设计 重构循环-不断发现架构中的缺陷和未澄清的需求,改进设计

    2022-09-21

  • davix 👍(0) 💬(0)

    如此看TDD驅動的實現方向是按測試金字塔從頂到底?從端到端到UT?

    2022-05-03