我开始学习 TDD,起因是工作中出了一个大 Bug。我在日常工作中会接触到一些和财务相关的业务,完成此类工作时一直都特别小心谨慎,生怕出错。工作很多年,一直风平浪静,直到有一天运营部门的同事告诉我,管理后台显示打给用户的金额比正常情况多了几十万!
一个价值几十万的符号
完整故事是这样的:我们公司的产品是一款语聊 App,聊天室内用户赠送给主播礼物,主播收到礼物后可以获得报酬,主播报酬 = 礼物价值 ✖️ 主播分成比例。
本次需求是根据主播评级动态来获取的主播分成比例,正常逻辑是先从 Redis 获取。如果 Redis 中没有,就再从数据库获取;如果数据库中也没有,就返回一个默认值,并保存至 Redis。
问题就出在默认值上。正常情况下,主播分成比例应该小于 100%,但由于我的失误,返回了一个无意义的值 -1,在计算主播收益时,所有参与运算的数据都取绝对值后再运算,这样 -1 就变成了 1,主播分成比例也就变成了 100%。
当财务部门给主播结算报酬时发现要多出几十万,这才发现不对劲儿。往日不起眼的小 Bug,瞬间升级成了重大 Bug,幸亏发现及时才没有造成更大损失。
反思
修复 Bug 就是一瞬间的事,-1 改成 0.6 就完事了,但今后如何避免类似的问题再次发生呢?这是一个难题。从上面的例子开始反思,我发现了这么三个问题:
- 认真的工作也会写出 Bug;
- 通过 Debug、日志、逐行看代码排错,太耗费时间,也容易出错;
- Code review 时,自己很难找出自己的 Bug。
关于上述问题,我首先想到的是通过单元测试解决,因为我曾听过 TDD 的传说:敏捷开发与极限编程中都在使用测试驱动开发,先写单元测试再编写实现代码,不但能实现自动化测试,还能带来很高的测试覆盖率。那么我就可以通过测试用例找 Bug,而不是凭着自信觉得没 Bug。
更简单地说,学习 TDD,可以让我写代码写得更心安些。于是我打算自学 TDD。
摸索着学 TDD
我首先是在网络上搜索一些视频,看看大家是怎么用 TDD 开发的,但是大部分资料教的都只有简单的几步:先写一个测试,运行一下没有通过,接下来实现被测功能,再运行一些测试,通过。
刚开始接触的时候感觉好厉害,但看得多了就感觉有点不对劲儿:TDD 的完成过程是红 / 绿 / 重构,但是这些视频资料里怎么都是到绿就结束了啊!重构呢?重构在哪里?这是真正的 TDD 吗?
还是看书靠谱儿,不如去看看书里是怎么做的吧。我先跟着《Java 测试驱动开发》《测试驱动开发的艺术》两本书练习,同时在项目中使用书中所讲解的 TDD 技巧进行开发。
起初光思考怎么写测试这个问题,就会占用大部分的开发时间,相对于之前的开发进度也慢了不少。经过一段时间的实践与思考,找出了我编写测试代码非常吃力的原因。
第一,方法内包含了太多功能,违反了单一原则,所以导致构造测试非常困难。
第二,在对自己刚完成不久的代码增加新功能时,起初无脑的复制粘贴总是那么轻松愉快,只需拷贝代码后再稍加改动就能完成。不过产品的思维总是天马行空,很快类似功能远远超过 3 个版本。例如根据时令水果调配的果茶,为了更好的口感与较高的性价比需要经常更换水果、包装、宣传海报,还有最捉摸不透的促销打折活动。
所以越往后编写测试代码,就越需要要花更多精力找出这些被测逻辑的相同与不同之处。当时我就在想,如果能做到 DRY 原则(不要重复你自己),就可以解决这样的烦恼了。
第三,和身边的同事、朋友分享通过 TDD 能让重构落地的实践时,大家要么摇头说听不懂,要么说时间紧,哪有时间写测试。有时我也会有疑惑:难道“代码能跑就不要动”这句话是对的?甚至怀疑自己,我真的是在进行 TDD 吗?TDD 是我这样的吗?怎么不像传说中的那么神奇?
正当我四处寻找如何提升 TDD 技能的方法时,《徐昊 · TDD 项目实战 70 讲》为我打开了系统学习 TDD 这个技术的大门,让我接触到了真正的 TDD!
突然出现一扇门
首先吸引我的就是视频形式的教学。要知道,学习代码,最好的方式就是手把手带练。而八叉老师在视频中边讲解边使用 TDD 开发这种方法,让我有了类似于结对编程的学习体验,以第一视角来观看徐老师的做法。
学习历程
看了第 1 讲的视频演示后,我被八叉老师通过 Idea 进行一波又一波的重构操作惊呆了,第一次知道原来还有这么多这么强大的功能!
等看到第 3 讲时,不仅有接连不断的震惊,更意外地是感受到了极限编程的气息!极限编程之所以叫“极限”,是因为它的理念就是把好的实践推向极限:如果程序员写测试是好的,我们就尽早测试,推向极限就是先写测试,再根据测试调整代码,这就是测试驱动开发。
此时我光环加身,看完视频后就想按照自己的想法去实现,以为到最后,结果能和老师的一样。没想到每次都差那么一点,但总体来说还是可以跟着老师的思路继续开发。然而到了第 4 讲,老师的一波神操作,让我彻底懵了!
就是几个小细节,让我明显感受我和老师的代码,已经是初级和神级的区别了:一个是传递参数类型不一样,老师的参数可以容纳更多的类型;另一个是老师做了详细的 TODO List,我只有大概的 TODO List;到最后,老师的返回结果按预定设计实现了华丽的变身,我的到最后一步卡住了,不能变了。没想到跟着老师写代码,竟然还翻车了!
我索性完全照抄老师的代码,先模仿,好在效果还不错。在观看八叉老师大量视频演示的过程中,我也收获了几个口诀:Happy Path(测试正常功能);Sad Path(测试异常功能);Default Path(测试默认值)。
按照这样写代码很容易做到分离关注点,一次只做一件事,化繁为简,轻松提高代码质量;Sad Path、Default Path 也可以帮助我更简单的处理非正常逻辑,写出更健壮的代码。
有了前车之鉴,在进入“实战项目二|RESTful 开发框架”的学习后,我也都是老老实实照着视频敲代码。
以为这样就没事了?这只是幻觉。
有时我颠倒了上下行的代码顺序,有时我将代码放在了循环外或者循环内(和老师不一样的地方)。通过解决这些问题,当你以为我对解答问题的思路有进一步了解时,你错了!我只是单纯的靠蛮力发现了代码哪里不一样(就像之前有一个游戏叫“大家来找茬”)。
不过,你要是以为我会一无所获并浪费时间时,其实到这个项目结束之时,我发现自己已经体会到 Inline、提取方法、提取变量(局部、方法参数、类变量)这些常用的重构手法,并已经能熟练地运用在日常开发中了。
在学习项目二的过程中,我的另一个收获是:工作中真正需要的是功能测试,而不是刻板的单元测试。
例如一个方法中包含数据库的更新操作,按书中单元测试的要求,“是不能依赖外部数据库,需要通过 Mock 代替”,而功能测试则更更加关注执行结果,重点验证执行结果是否达到预期,至于是否连接数据库,并没有限制。
我也曾经用 Mock 代替数据库的更新操作,单元测试都通过,但上线后依然出了 Bug,原因是 SQL 写错了,之后我便不那么刻意追求单元测试,而是更有效的功能测试。
进入“实战项目三|RESTful Web Services ”的学习没多久后,我就开始偷懒儿,没有完全跟着写代码,对解题思路也常常是一知半解。不过坚持下来,也收获了 JUnit5 中通过 DynamicTest 简化测试的技能。
视频中老师将百行左右代码缩减到了几十行(@TestFactory + 标签 + 反射刷新了我对自动化测试认知!)。工作中我将上千甚至上万行测试代码缩减到了几十行,解决了一个之前不可能完成的功能测试!
虽然没有完全学会 TDD,但我已经收获了很多,并且在日常开发中对我产生极大的帮助了。课程容量的确很大,我想如果有时间能再多练习几遍,把视频再多看几遍,收获肯定不止于此。
惊喜不断的读者交流群
最后我想特别分享一下课程的读者交流群。虽然咱们课程评论区并不怎么活跃,但读者交流群中时不时会掀起一波又一波打破我认知的讨论,还时不时出现几位平易近人的大佬来解答群友的提问,使我获益匪浅。
我把群里的讨论,按照自己的学习视角进行了记录,链接在这里 https://wyyl1.com/post/19/wq/。同时,我也将其中几个“虽然字少但感觉很有道理”的讨论分享在这里。
比如八叉老师分享的 Thoughtworks 小巨人项目用的书单,据说这是 TWer 入职一年就需要阅读完的量。书单截图如下:
有同学问是否有技术洞察的方法论可供学习,八叉老师的回答是这样的:
技术洞察啊,把过去 15 年所有重要文献读一遍;然后不明白的,自己试,很容易就建立洞察了。有哪些重要文献呢?找一本最近出的书,连书带参考文献,看一遍;再连带出参考文献的参考文献,继续看;然后继续,直到都看完,不就知道了?肯定漏不掉。
八叉老师的一些观点也很犀利和精准,比如:
结对编程 + TDD 大约会增加 15-20% 的成本,但是能提高 70% 的代码质量,减少反攻和上线时间。
八叉老师还曾经为 TDD 改编了祷告词:
TDD,愿人们都尊你的名为圣,愿你的国降临,愿你的旨意行在产品测试,如同行在开发。
我们日用的测试,今天赐给我们,免了我们的技术债,如同我们免了人的债,不叫我们遇到恐惧,救我们脱离凶恶。
因为国度、权柄、荣耀,全是你的,直到永远,阿们。
小结
这门课程是把《重构:改善既有代码的设计(第 2 版)》这本书拍成了电视剧,学习之后可以做到 8X 老师在这本书中的赞誉里提到的“先做对,再做好”,使用这种思路极大地简化问题。
如果你不想看书,可以尝试一下看电视剧!
最后附上我的学习清单:
- 《Head First 设计模式》
- 王争老师在极客时间的专栏 《设计模式之美》
- 郑烨老师在极客时间的四个专栏 :《软件设计之美》《代码之丑》《10x 程序员工作法》《程序员的测试课》
- 《Java 测试驱动开发》、《测试驱动开发的艺术》
- 《代码整洁之道》、《修改代码的艺术》、《重构:改善既有代码的设计(第 2 版)》