TDD 改变了我的生活

2019 年 5 月 24 日

TDD 改变了我的生活

现在是早上 7:15,为给客户提供支持,搞得我手忙脚乱。我们刚刚在《早安美国》节目上推出了新特性,很多初次使用我们产品的客户都遇到了 Bug。


所有的问题都暴露出来了,为避免错失大量新用户,我们决定紧急进行 hotfix 来解决问题。我们的一个开发工程师已经修改了代码,他认为这能解决这个问题。我们把修复好的测试链接发送到公司的聊天群里,让每个人帮忙验证一下这个问题,然后准备发到生产环境中,让人欣慰的是,问题解决了。


我们的 ops 团队启动了他们的部署脚本,几分钟后,hotfix 的内容成功发送到线上了。就在这一瞬间,客户支持的电话增加了一倍。我们的 hotfix 破坏了他们的其他功能,我们的开发只能在抱怨声中重新提交了代码,然后运维的同事回滚了 hotfix 的版本。


为什么需要使用 TDD (Test-Driven Development)


我已经有段时间没有更麻烦解决上述的这种情况了,几乎我工作过的每一个团队都有使用 TDD 的规范要求,不仅仅是为了让程序员不犯错,还有包括对未来开发的约束。当然 Bug 还是会有,不过发布到生产环境的版本已经将 Bug 数降到了几乎为 0 的水平,虽然软件变更和维护的工作量成指数上涨了。


当有人问我为什么要使用 TDD 的时候,我都会想起这个故事。我选择 TDD 的最主要原因是提高测试的覆盖率,使生产环境中的bug减少40%-80%。这是 TDD 带给我的最大的益处,就好像从肩膀上卸下了一个重担一样舒服。


TDD 不惧怕修改


在我的项目里面,我们的自动化测试和功能测试,足可以防止每天因代码修改发生的灾难性事故。例如,我正在看上一周 10 个类库自动升级的内容,我常常偏执地担心合并代码会破坏掉什么。


这里我用打猎来类比一下过去的流程:你会突然打开 Github,查看最近的合并请求,然后就发现“错误”了,这就像打到“猎物”一样。以上描述的流程现在被自动化的进程所替代了。代码会自动发布到生产环境,我也不会去手动查看任何一个合并的内容,也没什么需要我担心的,自动化的测试流程会帮我识别错误的内容。尽管你也可以在测试覆盖率不高的情况下正常使用这种流程,但是我不建议这么做。


什么是 TDD


TDD 的全称是测试驱动开发(Test Driven Development),流程如下:



  1. 在实现代码之前,先写一些判断实现的内容是否正确的代码。在进行下一步之前,查看一下失败的测试(这样的话就能识别接下来通过的测试,是否是真的通过了,了解如何测试我们的测试代码)。

  2. 编写实现的代码,查看通过的测试代码。

  3. 如果有必要的话再重构,这时你应该已经有信心去重构代码了,因为你有一个测试来告诉你,你是否破坏了什么内容。


TDD 如何帮助你节省开发时间


从表面上看,编写所有这些测试代码好像都是多余的,它们会占用很多额外的时间。期初,我也是这么认为的,我一直努力去理解如何编写可测试的代码,努力去理解如何为已经写好的代码增加测试。


TDD 是有学习曲线的,在上升初期的时候,你常常会增加15%-35%的额外开发时间。但当你使用了大概 2 年左右之后,你会发现一些神奇的事情发生了,相比之前不写测试代码,速度竟然提高了很多。


几年前,我曾想制作具备按照一定范围剪辑视频功能的 UI 界面。想法的思路是设置一个开始和结束的点,然后当用户通过链接,到达这个部分内容的时候,不会链接到整个视频上,而是直接链接到指定的位置上。


但是我写的代码并没有奏效,播放器会直接到达剪辑的尾部,然后继续播放,我毫无办法。


我认为主要原因应该在无法连接到事件监听上面。我的代码是这样的:


video.addEventListener('timeupdate', () => {  if (video.currentTime >= clip.stopTime) {    video.pause();  }});
复制代码


修改、编译、重新加载、点击、等待、重复。


每一次修改都会用大概 1 分钟进行测试,我尝试了很多滑稽的事情(大多在 2-3 分钟之间)。


难道我的"timeupdate"单词拼错了么?是否调错了 API?“video.pause()”这个方法是否真的有效? 基于上面的怀疑,我做了一个修改,添加了“console.log()”,返回到浏览器里面,刷新,点击到结束之前的任意一个点,然后耐心等待它走到结束。在“if“语句中,输出日志的语句没有执行。好的,这是一个线索。我从 API 的文档里面复制粘贴了“timeupdate“ 这个单词,保证他一定不会拼错。刷新、点击、等待。然而什么也没有发生。


最后我把“console.log()”放到了“if“外面,我想:“估计也没什么效果”。毕竟,“if“的代码这么简单,我应该没有搞错。可是他反而输出了内容。吓得我把咖啡都撒到键盘上了,无语!


墨菲定律的调试法则:有些东西你深信他不会出错,所以你没有测试它。但往往最后,当你想过了所有可能之后,回过头来你会发现,就是这里错了。


我设置了一个断点,想看看到底怎么回事。我检查了“clip.stopTime”的值,结果是 undefined,我回头检查了我的代码,发现当用户点击停止时间的时候,会放置一个小的图标,但是并没有设置“clip.stopTime“。“我简直就是白痴,为啥把我这么蠢的人放到电脑前面写代码,只要我活着,就应该让我远离电脑!”。


多年之后,因为当时的感受我仍然记得这件事,我想你应该能够理解那份懊恼,我们应该都有类似的经历,我们都被这种问题困扰过。



时至今日,如果我再写那段 UI 的代码,我可能会先写下如下的内容:


describe('clipReducer/setClipStopTime', async assert => {  const stopTime = 5;  const clipState = {    startTime: 2,    stopTime: Infinity  };  assert({    given: 'clip stop time',    should: 'set clip stop time in state',    actual: clipReducer(clipState, setClipStopTime(stopTime)),    expected: { ...clipState, stopTime }  });});
复制代码


表面上看,代码似乎不如“clip.stopTime = video.currentTime“ 来的简单,但这是一个要注意的点,这种写法是一种规范。代码文件可以证明我们的代码是否按照书写的内容执行。所以,当我们修改了设置停止图标时,x,y 坐标的位置,是不需要担心他会影响到剪辑视频的代码的,因为他们的代码没有任何关联性。


想给上面的代码写Unit Test么,可以看一下这里


主要的关注点不是代码要写多长时间,而是当出现问题的时候,需要多长时间来调试。如果代码被破坏了,这个测试的代码会给我一个错误的报告,我就可以马上定位到不是事件处理程序出现了问题,问题应该在“setClipStopTime()“ 或者“clipReducer()“ 里面,因为有实际输出结果和预期结果,所以我也知道应该怎么处理。更重要的是,未来 6 个月之后的同事如果修改这部分内容,也同样知道该如何处理。


我在每个项目中,第一件要做的事,就是设置一个监听脚本,他可以帮我在每次修改文件的时候,自动执行测试脚本。我通常会使用两个显示器,其中一个显示器会显示执行脚本的结果,另一个用来写代码。这样当我对代码修改之后,在 3 秒左右之后,我就可以看到我的修改是否是正确的。


对我来说,TDD 不仅仅是安全这么简单,它还包括了可持续的、快速、实时反馈的特性。当我做对的时候,会给予我满足感,而当我做错的时候,会及时给我指出来 bug 在哪里。


TDD 教会了我如何编写更好的代码


在这里我要承认一件令人尴尬的事:我在学习 TDD 之前,并不知道如何去构建一个新的应用。为什么我会被超出我能力的岗位所雇佣呢?但是当我面试了众多开发者之后,我可以非常自信的告诉你,很多人也都是这样的。TDD 教会了我所有关于有效解耦和软件组成(包括模块、方法、对象、UI 组件等待)方面的知识。


原因主要是单元测试会迫使你单独的测试每一个组件。给予一些指定的输入数据,单元测试应该返回预期的输出内容,如果与你的预期内容不符,则证明测试失败。关键是他应该独立在你的程序之外。如果你正在测试一些状态逻辑,你应该可以在不通过显示屏显示数据或者写入数据到数据库的情况下,进行测试。如果你在测试 UI 的效果,应该能在不访问网络和加载浏览器的情况下进行测试。


除此之外,TDD 还告诉我,如果你可以尽可能的减少使用 UI 组件的话,你的工作生活也会变得更容易。把业务逻辑和 UI 隔离开,实际上,这意味着如果你在使用类似 React 或者 Angular 这类的 UI 框架,将创建显示组件和容器组件分离开,这样会对你很有益处。


对于一些显示组件,给定一些属性,保持相同的状态。这样的话就可以很简单的进行单元测试,确保属性的值传递正确,而且 UI 的布局逻辑执行正常(例如,如果一个 list 结果是空,那 list 组件就不需要展示,否则的话就要把内容填充到组件里面)。


在刚学习 TDD 的时候,我就知道需要关注拆分问题这件事,但是我并不知道该如何拆分问题。


单元测试教会了我使用 mock 的方式进行测试,用代码的方式实现mock,超乎了我的意料,改变了我处理软件组件的方式。


所有的软件开发的内容都是组件,大问题可以拆分成小问题、易于解决的问题,把他们的解决方案组合在一起,形成应用。通过 Mock 的方式进行单元测试的主要原因是,你理解中的原子性的组件其实不是真正意义上的原子性,相互之间会产生影响。在不减少代码覆盖率的情况下,最好减少使用 Mock 的方式进行测试。在整个过程中,你就会慢慢发现各种潜在的耦合在一起的代码。


TDD 让我成为一个更好的开发者,并指导我如何编写更易扩展、维护的代码,来支撑复杂的业务和大型分布式系统的需求。


TDD 如何帮助我们节省整个团队的时间


我之前提过了,测试之前要保证测试代码的覆盖率。我们在编写实现代码之前,先要编写测试代码,这样就可以验证我们写的实现代码是否正确。首先,先写测试代码,之后执行发现报错,然后编写实现代码、失败、测试通过、重构、反复重复直到完成。


整个流程构建了一个足够安全的保障,bug 极难从中穿过。这个安全保障带给了团队足够的信心,让大家不再惧怕合并代码。


在足够成熟的测试代码和测试覆盖率的基础下,可以让你的团队成员放心在基础代码库上面提交任何大小的修改,并让你的程序茁壮成长。


消除对修改代码的恐惧就像你要给机器加润滑油一样,如果你不这么做,那机器就会慢慢停下来,直到你把它擦干净,打开开关,它才会嘎吱吱地继续运转。


如果没有这种恐惧的话,开发的节奏就会很流畅。提交代码,你们的 CI/CD 工具会执行测试代码,如果测试失败,它会暂停下来,高呼“出错了出错了”,并指出到底哪里出了问题。


这会令你的开发过程焕然一新。


查看英文原文:https://medium.com/javascript-scene/tdd-changed-my-life-5af0ce099f80


2019 年 5 月 24 日 08:006137

评论

发布
暂无评论
发现更多内容

第十一周作业

solike

读写分离这个坑,你应该踩过吧?

楼下小黑哥

MySQL 主从同步 读写分离

2020 阿里云原生实战峰会即将开幕 云原生落地的正确姿势

阿里巴巴云原生

阿里巴巴 阿里云 开发者 云原生 实战

通过docker获取系统运行情况的实用命令

晓川

Bitmap为什么那么快?

Man

redis 中间件

面试被问线程安全怎么保障,我的回答让面试官眼前一亮

996小迁

Java 架构 面试 多线程

Spock单元测试框架实战指南四 - 异常测试

Java老k

Java 单元测试 spock

智慧警务系统开发解决方案,大数据可视化平台建设

WX13823153201

智慧警务系统开发

数据结构与算法系列之散列表(一)(GO)

书旅

go 数据结构 算法

一个依赖搞定 Spring Boot 反爬虫,防止接口盗刷!

Bruce Duan

反爬虫组件 kk-anti-reptile

拆解增长黑客之实战(二):留存与变现

丁一

读书 增长 产品运营

架构师训练营第一期 - 第十一周课后作业

卖猪肉的大叔

极客大学架构师训练营

当我们谈前端性能的时候,我们谈的是什么

vivo互联网技术

性能优化 前端 前端性能优化 页面

算法训练营总结

陈皓07

5种分布式事务方案与阿里的 Seata 中间件

Bruce Duan

分布式事务 seata

第六周-学习总结

Mr_No爱学习

架构词典:语言

lidaobing

架构 语言

排查指南 | 当 mPaaS 小程序提示“应用更新错误(1001)”时

蚂蚁集团移动开发平台 mPaaS

小程序 问题排查 mPaaS

第六周-作业1

Mr_No爱学习

Redis 子进程开销监控和优化方式

码农架构

Redis开发与运维

警察营救安徽望江县17岁女生跳河自尽过程中,现场看热闹的旁观者们在做什么?

wbliu85

「生产事故」MongoDB复合索引引发的灾难

Kerwin

数据库 mongodb

悟空活动中台-打造 Nodejs 版本的MyBatis

vivo互联网技术

Java 前端 mybatis nodejs

为什么建议使用你 LocalDateTime ,而不是 Date?

Bruce Duan

LocalDateTime Date

报销发票抵扣工资的CTO,该不该? | 法庭上的CTO(5)

赵新龙

CTO 法庭上的CTO

生产环境压测建设历程之三 淘宝网2009年的痛

数列科技杨德华

S型曲线不止关乎身材?|技术人应知的创新思维模型(2)

Alan

创新 思维模型

话题讨论 | 作为地地道道的程序员半年内都没摸过代码是什么样的体验?

xcbeyond

话题讨论

《前端算法系列》数组去重

徐小夕

Java 面试 算法 前端

(G20200388020528)第一周练习

走走,停停……

Redis 持久化方式-RDB

码农架构

redis redis持久化

TDD 改变了我的生活-InfoQ