浅谈单元测试和重构
隐喻
年纪大了,腿脚不利索,拄着拐杖走路,走的稳不说,还能预防跌倒。
但如果真的跌倒了呢?
跌倒后有没有人敢扶?扶起来还能不能走?如果能走,走得还能不能像以前那样快?如果不能走,是不是要去医院?
没了拐杖,产生了灾难性的骨牌效应。
意义
单元测试之于开发人员,相当于老人的拐杖,离开拐杖,也能走,但就是深一脚浅一脚,而且还有随时跌倒的危险,跌倒之后还会产生一系列的问题,搞的人焦头烂额。
在没有单元测试支撑的情况下写代码,你不会知道代码里面有没有逻辑BUG,是否符合预期,不能发现编码过程中引入的错误,更不能发现设计和需求中存在的问题。
我曾经所在的一家公司,开发人员是不写测试的,他测试自己代码的方法,是写完之后运行一下,点点看有没有问题,效率低下容易漏测不说,还很业余,简直侮辱软件工程师这个高大上的职业(看我鄙视的眼神)。
假设你写了一个服务,每写一个方法你都写单元测试将方法中的路径全覆盖掉,如异常,if-else, switch语句等,刚准备提交,突然接到通知,需求变了,你不得不改动编码逻辑,这个时候怎么办呢?首先要跑一遍单元测试,确保你当前功能的测试全跑通,然后分析需求,按照要求对相应的代码进行了修改,之后,修改或增加单元测试对改动代码进行覆盖,你点下了开始按钮,看着所有测试都打了绿色的对勾(✔),你从容淡定的提交了代码,拿起菜刀,哦不,端起水杯,跟分析师说:改好了!你看下是不是酱婶儿的?
我们一起来看看刚才发生了什么。
首先,因为编写了全路径覆盖的测试单元,所以你不怕需求变更,也就是说,单元测试对于重构或者需求变更来说,能让我们对编写的代码更有信心。
其次,可以预期遇到的错误,是冒泡交给上层逻辑还是在方法体内消化掉,都在你的掌握之中。即便我们编码或者写测试的过程中没有发现这些异常,有想不周全的地方,后面遇到了这个异常,也可以修改测试将其覆盖到测试里面。我们对自己写的程序有了全面的了然于胸的掌控。
原则
比较完备的研发团队,一般都会配置测试部门,在接到需求文档之后编写测试计划,开发人员结合需求文档,阅读测试计划,可以很快理解项目的流程和操作场景。甚至有经验的开发可以直接依据测试计划使用TDD模式快速实现。
但大多数开发还是使用的编写逻辑边单测的模式,所以这里说一下在业务支撑型项目的开发中,我们写单元测试的原则,并简单介绍一下常用方法。
首先要说到的,是项目和数据依赖问题,这个问题分析起来,首先要问你这个测试方法的测试目标是什么?是测试代码的业务逻辑还是测试项目依赖的元数据。如果是前者,我们完全可以编写面向业务逻辑的测试单元,数据部分则用Mock解决。但如果是后者,你需要确保在测试、生产等环境里项目依赖的元数据存在才能正常运行,那么,可以专门编写测试单元来检查当前环境下是否存在该元数据的定义及存在预期的数据定义。这个例子中,一个是检查代码逻辑,一个是确保项目正常运行,是两个不同目标的测试单元,所以,一个要用到Mock,一个需要检查环境。
然后我们说外部接口和项目依赖。这个问题比较常见。对于所依赖的外部接口,我们必须假定它是不稳定的,编码时要做好降级准备(如接口无响应,返回值错误等情况的处理),给用户一个友好的反馈。那么怎么测试呢?
同样,需要分别写两个测试单元,一个用来检查调用外部接口的方法的返回值,是否符合外部接口的期待;另一个测试单元用Mock模拟外部接口返回的各种结果来检查我们的后续逻辑。
为什么不直接调用外部接口测试呢?那是别人家程序员的任务啦!
标准的团队都会配置一个测试小组,开发任务完成后,提交测试。测试组的同事使用各种姿势虐待我们的项目,将找到的BUG提交给我们进行修复。这个时候,我们第一件要做的事情还是编写单元测试,去复现这个BUG,然后再去修改代码,直到BUG不再出现。这也是对测试同事的尊重,保证你再次提交代码后,这个BUG不会重复出现。
写单元测试不会导致延期交付
这句话听起来有点反人类。但事实上,编写单元测试不但不会造成项目延期,反而会使我们的工作目标更明确,从而使编码工作更高效,最终往往会提前完成目标。
为什么会这样呢?因为目标很明确,你会少写很多臆想的代码逻辑,再加上这些猜测着写出来的代码还可能存在BUG,后果就是你花了很长时间在调试修改一段不必要的逻辑。
随着迭代的逻辑越来越复杂,因为有着测试的支撑,每次修改和功能的增加,变得轻量而敏捷,优雅而神秘。
一切不以重构为目标的单元测试都是耍流氓
可能有同事觉得“重构”这个词太过恐怖,认为只要是重构,就是对程序结构、数据结构的推翻重来。但那些注重成长的程序员非但不会认为重构恐怖,还会将重构作为成长路上的最好的伙伴。
重构的频率,最好是每次提交代码前,对自己的代码进行一次自审。问自己这段代码是否过于繁琐?有没有更简洁的方法?它的可读性好不好?可维护性怎么样?如果代码量比较大,或者你正在审查自己多个里程碑的代码,可能还会问自己关于设计原则和模式方面的问题。
重构的间隔太长,会演变成严重的技术债务,而这些技术债务的偿还,才是本节开头所说的,对程序结构、数据结构都会有很大的改动,相较于每日自审重构,复杂度、数据风险以及开发成本都会成为令人头疼的问题。尤其是当你去重构“前任”留下的代码时,那心情,很是酸爽。所以,为了不让我们的“后来者”酸爽,请坚持每次提交前的自我审查和重构,当然,前提是你写了单元测试。
一个事实是,用户的需求随着业务需要一直在变化,所以我们的程序也一直在迭代,要想保证我们每次的迭代又快又稳定(或者调整现有功能,或者增加新的功能),我们必须给自己一个安全措施,这就是单元测试。
所以,不要怕重构,每次提交前审查代码->重构代码->执行单元测试,应该成为我们每次提交前固定的工作流程。
单元测试和重构,成长路上的好伙伴。