每个人对待单元测试的态度各异,有些人觉得单元测试很重要,有些人觉得单元测试可以不写;我对单元测试的态度是肯定的。
存在的问题
进入新团队半年多了,感觉团队小伙伴对单元测试的写法及认知存在不合理的地方,比如很多单元测试就是调用一下方法,没有对数据进行构造,也没有数据回滚,也没有对期望结果的比对,单元测试的正确性靠打印或者数据库数据查看,这种单元测试其实在很多情况下是不可重复执行和完全自动化的。
单元测试就是走下逻辑流程,没有对边界数据做单元测试;比如方法参数传入为null,一些必须为正数的,传入负数,都没有对应的单元测试方法;一句话来说:就是测试覆盖率低
单元测试加载的东西太多,跑起来加载一堆不必要的服务,导致单元测试跑起来慢
最近在整重构的事情,顺便把单元测试这块整理了一些,并在团队中做了一次分享;
单元测试对代码提出的要求
方法职责单一原则
可测试性
- 方法职责单一好理解,就是一个方法只干一件事情,还有一个就是项目结构分层清晰,每个类或接口按照领域划分
- 而可测试性,其实方法职责单一的代码可测试性就很好
单元测试应该怎么做
基本原则,单元测试的方法只负责被测方法的正确性,比如一个下单业务逻辑的方法,不应该涉及到数据库层的方法,这些方法应该被mock,因为数据库层的方法由数据库层方法的单元测试来保证方法执行正确性
先介绍下目前项目的结构
- repo层(数据库操作层)
- service层(领域化的方法,比如orderService,提供订单相关操作的方法服务)
- biz层(业务逻辑层)
- controller层(该层主要用来接收参数和从request获取头信息)
我是如何写单元测试的
- 首先repo层:repo层主要负责数据库层的操作,对repo层的单元测试需要与数据库交互;
- 第一、由于数据库结构的一些原因,比如唯一键的设定,就会导致单元测试只能跑一次,这个时候需要有回滚机制的单元测试,在单元测试跑结束后,数据库事务回滚(spring-test框架有提供这样的功能,实现机制就是将单元测试当做一个事务,事后全部回滚,至于代码,网上很多,这里就不放出来了);
- 第二、很多时候没有单独的测试库用来单元测试,这个时候对单元测试的数据构造有一定的要求,我的做法一般是造一些罕见的数据,比如id,我就用9999999,一个基本不会在开发库出现的数据,而且第一点的数据库事务回滚机制也发挥作用;
- 第三、repo层的单元测试,比如一些查询的,往往需要预先插入数据,再来验证数据的正确性(junit的Assert断言),这个时候也需要第一点的事务回滚机制
- 第四、需要对一些异常及边界情况进行测试,如果唯一键的重复问题等
- 讲到这里,加载太多服务的问题也基本解决了,repo层的单元测试只加载repo的bean,其他的一些bean依赖就不加载了,瞬间就轻量化了
- 再是service及biz层,service层和biz层基本是一样的,就拿service层来举栗子吧
- service层一般情况下调用repo层,那这时候单元测试的时候,是不是就又会调用repo层的东西,而biz层会调用service层,service层再调用repo层,不知不觉单元测试又要加载一堆的bean依赖,基本跟起个web服务差别不大了
- 怎么办呢,mock排上用场了,单元测试的mock框架也不少,我就选择了比较常用的mockito;从理论上来说,service的逻辑不应该跟repo层有任何关系,因为repo层方法的正确性由repo层的单元测试保证了,在service层做单元测试的前提就是假设repo是没有问题的,这个时候需要将repo层的调用进行mock操作,然后再对service层的方法本身的逻辑进行验证;而同样的biz层因为是调用service层的方法,service层的方法已经由service层的单元测试保证正确性,那么biz层的单元测试需要将service层的方法调用进行mock;
- 对于加载依赖太多的问题,service及biz层采用mock的方式,基本就不需要加载bean了,跑单元测试就不会那么慢了;
总结
讲了这么多,单元测试非常依赖方法的可测试性,一段很长复杂的代码是难以单元测试的,单元测试对业务代码的质量提出了要求,从另一方面促进开发人员编写出更加优美的代码;那有同学要问了,单元测试跑的没问题,怎么保证集成是没有问题的呢?首先单元测试的覆盖率基本可以保证,再者,测试同学的自动化测试脚本来对集成代码负责;
文字比较多,今天在这里讲解的主要是单元测试的思路,而不是实现,有些地方会显得比较啰嗦。