Day 18 「春暖鸭先知」TDD 来了

古语有云:「竹外桃花三两枝,春江水暖鸭先知。」春天不会早上起来敲你家门,跟你说他来了。冬天进入春天的过程,是一天一天变化的。等你发现春天来临时,外头早已开遍桃花了。

我们写程序时,结构在变「硬」有时快有时慢,但不论快慢,都不会「通知」你,都是你有一天要改一个小小功能,却发现竟然要做非常大幅度的修改,这时才赫然惊觉:「糟了,是世界奇观!不小心把软件写成硬体了!」


糟了,是世界奇观!图片截自脸书:高雄好过日

那怎麽办?

今天起的连续三天,我们要聊的是这几年大大改变笔者本身写程序习惯的开发方法:Test Driven Development,江湖人称 TDD。

起源

TDD 的发明,一般认为是 Kent Beck 在 2,000 年左右提出的,但其实 Kent Beck 自己後来也说,他不是创始者,他只是把一个已经有人在用,而他觉得很棒的开发方法系统化地整理出来,让大家更容易使用而已。不论如何,这个方法在那之後至今 20 多年来,确实改变很多人的 coding 习惯,包含他的好友,人称 Uncle Bob 的 Robert C. Martin。

Uncle Bob 曾不只一次在公开场合聊到他第一次见识到 Kent Beck 用 TDD 的方式,以「数秒 commit 一次」的方式,慢慢完成一项工作,而且做完的同时,程序、测试、重构,三件事情是同时完成的。并且不断描述当时他有多震惊与兴奋!总之在那之後,Uncle Bob 就成为了 TDD 的爱好者与推广者了。

近二十年後,我也是 XD

TDD 基本操作

TDD 主要是由三个动作组成的:

  1. 写一个错误的测试
  2. 使测试通过
  3. 重构

把一个功能,拆解成多个小小的验收条件,不断重复以上三个动作,直到功能完成,就形成了 TDD 的基本套路。为什麽说是「套路」?因为这只是一般形式,要真正在工作中用得上,没那麽容易。首先,每个循环要包含哪些内容,就是门学问了。这个我们下一篇会深入讨论,这里读者先了解原则即可。

上面所谓的原则,就是「红绿灯原则」与「Baby Step 原则」。


图片截自 http://www.anecon.com/blog/tdd-baby-steps/

在第一步依需求写出一个测试後,不罗嗦,直接跑下去。这时测试会过就有鬼了,我还没写程序哩!所以 fail 是正常现像,请安心服用。这时 IDE 一般会以红色标示,所以我们称之为「红灯状态」。

接下来要做的事,就是写程序让它通过。这时,我们要秉持 Baby Step 原则,「尽量少做一点事」,或是「少做一点设计」,只求「这个测项」能通过,其他可以先不管。这时再跑测试, IDE 会呈现绿色,进入「绿灯状态」。

此时很多人会直接进入下一个测项,而 Kent Beck 则建议我们且慢,先找看看有没有可以重构的地方。接着跑测试,因为是「重构」,根据 Martin Fowler 的建议,重构不应该改变物件对外表现,所以这时,测试应该维持绿灯,也就是全部通过才对。

请特别注意第三步的重构,很多人使用 TDD 会挂掉,就是挂在这里。在操作 TDD 时,因为每一步都很小,所以我们都以为没什麽好重构的。还记得前几篇中的「发奖学金」的例子吗?事实其实是,在加入新需求时,尽管需求看起来不大,但程序的「扩展」与「变丑」,是一下子的事。如果我们不经常地停下来看看程序的样貌,一看到坏味道就重构掉,等程序多完成一两个测项再回头来重构,很可能已经变得太丑而无力回天了。

原理

说到底,为什麽 TDD 会被设计成这个样子?因为它希望我们「随时」保持程序的可运行性与整洁度,并且不要过度设计。而这些目的是怎麽达到的?笔者阅读过一些书籍,加上自己一些经验与心得後,整理如下:

回想第一步,先写一个错误测试的原因其实有二。第一,我们想要先确保接下来要加的功能,在程序写完後,能满足需求,於是先用测试来描述它。一但测试通过,就能说明程序是满足需求的。第二,我们必须确保测试此时是 fail 的,因为当我们还没加程序,测试就通过了,这代表这个测试什麽也没测到,属於「无效测试」,应该立即删除,否则会损害测试的整洁度。很多人 TDD 越做越慢,测试整洁太差也是主因之一。

第二步,我们应该做「只能再多通过这个新测试」的逻辑。为什麽?因为多做事会多花时间呀!在下一个测试还没出生之前,任何「超前部署」都是多余而不实际的。

很多人喜欢在一开始就花大把大把的时间想好了完整设计,但事实上经常会动手做到一半後又脑袋一拍:「啊!我想到一个更好的解法了!」这时想到的解法通常会比一开始的还好,因为你手上握有的证据变多了,你更能做出较适当的判断。如果你一开始已经有了一个非常完整的设计,并且已经做了很多先期准备,那此时你要丢弃的内容会非常多,非常浪费。倒不如一开始不要想那麽多,大方向抓住就开工,还比较实际一点,也比较经济。

笔者的父亲多年经商,他经常告诫我们:「做出来的商品,最好是每一件都能卖出去,就能把效应拉到最大。」笔者虽然後後来没有克绍箕裘,但父亲的教诲还是有听进去的。

第三步的重构,目的就在「及早发现,及早治疗」。如同前文所述,更适当的设计总是「待会儿」才会出现。每当做完一点点事情,我们应该给自己一点点时间来做这样的审视与思考。这个一点点时间不用太久,通常二三十秒,就能够看出端倪。

如果此时发现更好的设计,就应该直接改掉,因为我们才刚刚写完逻辑,对整体结构非常熟悉,而且还有测试保护,此时正是重构成更好设计的最佳时间。

如果这时不做,心想着等等把所有功能都做完再来重构,恐怕到时坏味道已经累积太多,改都改不动,无法重构,只能重写了。如果不小心走到这一步,相信很多人就会选择放弃,就这样 commit 後上线了。等到一段时间需要回来改这一段程序时,就要花更多时间理解,更多时间修改,如果想重构,也就要花更多时间了。这些「更多时间」都是没有必要的花费,其实。

Kuma 表示

如果各位读者有一种概念,叫做「TDD 就是先写 test 再写 code」,那真的是有很大的误会。笔者认为,TDD 名字中虽有一个 Test,但本质上不是 Test,而是需求。就像每个 RD 都会想要「先确认需求」一样,TDD 希望我们做的事,是「先试着描述需求」。你必须得先能成功描述需求,让别人看得懂,或至少让 30 秒後的你自己看得懂,你待会才有凭据去说明你刚写好的 code 是「对」还是「不对」。

至於为什麽非得用 Unit Test 来描述呢?为什麽不写在纸上、Google Sheet 中,或是其他地方呢?笔者认为原因有二。其一,Unit Test的结构,为「准备、跑、对答案」,这正巧与需求描述的必须元素「前提、过程、结果」对应得上。其二,Unit Test 可以执行,而且「错了会叫」。写在纸上的需求错了可不会叫!

有了能不错地与真实场景对应,而且「错了会叫」的需求描述,RD 就可以放心大胆地对程序做任何修改与设计,反正错了自然有人会跟我说(而且是几乎马上)。因此,笔者认为,拿 Unit Test 来描述需求,不是硬规定出来的,而是随着开发者的经验累积,在想要不断提高效率与正确率的动力推进下,自然而然演化出来的结果。

每次与客户或 PM 沟通需求时你是否会想直接问「你现在情况如何?你想要怎麽进行?你觉得事情变得怎样会更好?」如果这样的沟通方式让你觉得舒服,且理当如此,那我认为 TDD 的开发方式应该很适合你。

谜之声:「能自然舒服地把事做好就好,不是 TDD 也无妨。」

Reference

  1. TDD:https://en.wikipedia.org/wiki/Test-driven_development
  2. Kent Beck, Extreme Programming Explained, Addison-Wesley Professional, 1999
  3. Kent Beck, Test Driven Development : By Example, Addison-Wesley Signature Series, 2002
tags: ithelp2021

<<:  [Day 4] Reactive Programming - 观察者模式Observer Pattern

>>:  索引合并(index merge)

Day13 NiFi - Variables & Parameters

今天要来讲的主题是 - Variables & Parameters。如果读者们还记得 Fl...

Day24 - 【概念篇】Keycloak使用基本概念 - 第一部分: Client

本系列文之後也会置於个人网站 Client与一些安全相关的设定 在OAuth架构下的Client(...

Day28:网页排名演算法(PageRank)

PageRank PageRank是一种连结分析演算法,它通过对超连结集合中的元素用数字进行权重赋值...

Day23 - 在 XState 中的平行式状态 Parallel States

还记得我们在 Day 13 的例子吗? 有个 Input 的 UI 元件,且它有以下 [Invali...

[Day-17] 二维阵列

今天要来延续上次练习的阵列 这次要练习的阵列跟上次的有点不一样 那就是一维阵列的进阶版「二维阵列」 ...