不同的人对单元测试的理解可能不同,本文中提到的单元测试是指同时满足下面两个条件的测试:
- 运行时间在0.1秒之内。(因为如果一个测试访问了数据库,文件系统或其它外部依赖,运行时间肯定超过0.1秒,可以参考《Working Effectively with Legacy Code》)
- 能自动验证产品代码的正确性。
我们从几年前开始推行单元测试,但是一直做得不好,每当想到单元测试,我就想到一次回顾会议上的一段对话,
小S:“这个任务估计2天完成。”
小A:“我们从这个Sprint开始鼓励写单元测试。”
小S:“等一等,写单元测试还需要1天,这个任务估算应该是3天。”
小A:“好吧。。。”
很多程序员的想法和小S是一样的,不愿意写单元测试是因为担心写单元测试要花费额外的时间,影响任务的按时完成。我们组织过《xUnit Test Patterns》的学习,也做了不少关于单元测试的分享,大家虽然都认同单元测试的重要性,但是回到项目组里面以后往往因为交付的压力就把单元测试搁在一边了,所以一直没能养成写单元测试的习惯。我们也尝试过TDD,但是感觉更难。相信很多公司和我们类似,Time and time again我问自己“到底怎么才能让大家都写单元测试呢?”
最近刚刚读完《习惯的力量》,这本书给了我很大的启发(有了锤子,什么都看上去像钉子?)。一个习惯有三部分组成:暗示,惯常行为与奖励,我首先想到了奖励,如果没尝到单元测试的甜头,为什么要写单元测试呢?那写单元测试的甜头和奖励是什么?
最近我们的生产环境连续两次出了一个相同的问题,一位同事为了让本地的集成测试通过,把一行产品代码注释掉了,测试完成后忘记把代码复原了,代码示意如下,
//下面代码是经过加工简化后的产品代码
Cost updatedCost = retrigerCost(order);
String costMessage = generateMessage(updatedCost);
//sendMessage(costMessage); //这行代码被注释掉了,因为本地没有配置消息服务器
经常写单元测试的人能一眼看出注释掉的代码可以变成一个“Seam”,我们在不改变产品代码的情况下可以通过改动“Enabling Point”替换代码的行为,不让它往消息服务器上发消息。(可以参考《Working Effectively with Legacy Code》)对于这个例子来说,写单元测试的奖励就是不用注释产品代码就能跑通测试,省去了检查,修复和部署生产环境的时间。
去年年初的时候,香港和珠海团队移交了几个项目给我们,因为大家对代码不熟,代码又没有单元测试(典型的遗留代码),每次改代码的时候都有如履薄冰的感觉,代码上线以后经常会出现已有功能被改坏了的情况,现在大家就怕在遗留代码上做改动。由此我想到的第二个奖励是用单元测试的思维去改遗留代码会更安全。
仔细想想,单元测试的奖励还有很多,比如我们现在基于UI的自动化测试只能覆盖最基本的流程,但是单元测试能以更低的成本覆盖更多的逻辑与数据多样性。再比如很多同事反映把代码部署到本地的应用服务器很慢,写单元测试能有效地降低部署的次数,节省部署的时间。
想完奖励,接着想暗示,那么写单元测试的暗示或者开关是什么呢?很自然的就是:
每当我们想注释产品代码的时候
每当我们想安全地改动遗留代码的时候
每当我们想自动化测试复杂业务逻辑的时候
每当我们想节省环境配置与部署时间的时候
。。。
有了奖励和暗示还不够,要让一个人做出改变,真正开始养成写单元测试的习惯还是很难的。就像我很早就想养成晨跑的习惯,但是直到体检医生说我脂肪肝的时候,我才真的开始晨跑。所以我们还缺一个类似“脂肪肝”的不大不小的危机。
Wise executives seek moments of crisis, or create the perception of crisis, and cultivate the sense that something must change.
危机是奖励的反面,但是我们可以利用不写单元测试带来的危机(比如每次发布新版本总会破坏已有的功能,影响客户业务的正常运作,给客户带来损失,造成赔偿或者客户流失。),向大家传递一种紧迫感,使大家感受到是时候养成写单元测试的习惯了。
总结一下,推行单元测试的第一件事情也是最重要的事情是找到它能带来什么奖励,然后找到写单元测试的暗示和开关,尽早地让大家切身地感受到这些奖励,最后利用不写单元测试带来的危机,让大家立即开始养成写单元测试的习惯,用习惯思维持续催化单元测试,让单元测试成为团队的新常态。