为什么要做单元测试
- 保证代码质量
- 一定程度提高代码合理性:当我们发现给一个方法写单测非常困难,比如单测需要覆盖的分支非常多,那可能说明方法可以拆分;又比如单测需要mock的调用非常多,那可能说明方法违背了单一责任原则,处理了太多的逻辑,也可以拆分等等。
- 有效防止回溯问题(regression issue)的出现:所谓回溯问题,指的就在之前版本没有在新版本才出现的问题。这种问题的严重程度是最高的,影响也是最恶劣的。原因很简单:用户可以接受一个本来在老版本就不存在的功能不可用,但是一定无法接受一个本来在老版本用地好好的功能突然失效了。在新功能开发完后,运行老功能单测,如果发现未变更逻辑的老功能单测报错,则很有可能是出现了回溯问题。
- 帮助测试人员确定回归范围:这一点其实是第三点扩展。在新功能提测的时候,开发人员需要提供测试范围,毕竟随着功能的不停增加,全量回归已经变得越来越不可能了。有些开发同学,为了安全起见,随意增加回归范围,这无疑增加了测试人员不必要的工作,是一种严重浪费测试资源的行为。在新功能开发完后,运行老功能单测,如果发现单测报错,则说明这部分老功能的逻辑可能发生了变化,单测需要进行相应的调整,且相关功能应该属于回归的范围
容易忽略的价值
- 改进代码设计
- 测试是代码文档
单元测试范围
- 覆盖范围应包括所有提供了逻辑的类:service层、manager层、dao层、自定义mapper等,甚至还有部分提供业务逻辑的controller层代码,覆盖范围不应包括自动生成的类:如MyBatis Generator生成的Mapper类、Example类,不应包括各种POJO(DO,BO,DTO,VO…),也不应包括无业务逻辑的controller类
- mapper层的测试需要借助内存数据库,例如H2
- 私有方法不需要跑单测,通过调用方法的单测进行测试
指导原则
单元测试是为了验证你有问题的,不是验证你没问题的
AIR 原则
单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
A:Automatic(自动化)
单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。
I:Independent(独立性)
保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。 反例:method2需要依赖method1的执行,将执行结果作为method2的输入。
R:Repeatable(可重复)
单元测试是可以重复执行的,不能受到外界环境的影响。 说明:单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。 正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring 这样的DI框架注入一个本地(内存)实现或者Mock实现。
BCDE原则
编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量
B:Border(边界值测试)
包括循环、 特殊取,边界值测试包括循环、 特殊取特殊时间点、数据顺序等
C:Correct(正确的输入)
正确的输入并得到预期结果
D:Design(与设计文档相结合)
与设计文档相结合来编写单元测试
E:Error(强制错误信息输入)
强制错误信息输入(如:非法数据、异常流程业务允许等),并得到预期结果
一些反例
- 二等公民:测试代码没有和生产代码一起进行重构、包含大量重复的代码,导致很难维护测试。
- 搭便车:而不是编写新的测试方法来测试另一个/独立的功能。
- 快乐之路:这些测试始终走在快乐的道路上(即预期的结果),而无需测试边界和异常。
- Line hitter:乍看之下,测试涵盖了所有内容,并且代码覆盖率工具可以100%确认它,但实际上,测试只是对代码进行了编码,而没有对输出结果进行任何分析。
- 本地英雄:一个测试用例,它依赖于特定的开发环境才能运行。结果是本地测试通过了,但在有人尝试在其他地方运行时失败。
- 串帮:必须按一定顺序运行的几个测试,即一个测试更改了系统的全局状态(全局变量,数据库中的数据),下一个测试依赖于此。
- 慢戳:运行非常慢的单元测试。开发人员启动测试时,他们有时间去洗手间,抽烟,或者更糟糕的是,在一天结束之前回家进行测试。
- 无名测试:随意的测试方法名, 例如test1,test2,test3。结果,测试用例的意图不明确,唯一可以确定的方法是阅读测试用例代码并祈求清晰。
- 模拟:当被测试的对象包含了太多的依赖时,在这种情况下,单元测试包含太多的mock,stub或fake,以至于根本没有对被测系统进行测试,而对模拟返回的数据进行测试。
- 沉默的Catcher:如果发生异常,则通过的测试。即使实际发生的异常类型与开发人员预期的异常不同。
- 过多的设置:一个测试,需要进行大量设置才能开始测试。有时,使用数百行代码为一个测试准备环境,其中涉及多个对象,由于所有设置的“噪音”,使得很难真正确定要测试的内容。
- 按方法测试:一个类对应一个测试类是可以的,但是一个生产方法对应一个测试方法,这不是一个好主意。
- 连体双胞胎:说是单元测试,实际上是集成测试,因为被测试系统和测试之间没有隔离。