单元测试
脏测试等同于甚至坏于没测试
测试必须随着生产代码演进而修改
单元测试使代码可扩展 可维护 可复用
测试类型
- 边界值测试 边界是最容易出错的地方
- 正确的输入 并得到预期的结果
- 错误的输入 得到预期的错误结果
重要性
- 保障质量
- 进行风险控制
- 限定最小爆炸范围
- 增加重构自信
原则
AIR
- automation 自动化
- independent 独立性
- repeatable 可重复
FIRST
- fast 测试运行要足够快
- independent 测试之间要相互独立
- repeatable 测试可在任何环境中重复通过
- self-validating 通过一个布尔值表示测试是否通过(自动化)
- timely 测试要及时编写 指的是TDD 测试在生产代码之前编写
粒度与范围
- 方法级别的粒度
- 针对测试区分case场景
- case职责要单一
- 核心功能覆盖 非核心随缘
单元测试与集成测试
集成测试运行通常更慢,很难编写,很难做到自动化,需要配置,通常一次测试的东西过多,并且集成测试会使用真实的依赖,而单元测试则把被测试的单元和其依赖隔离,以保证单元测试的高度稳定,还可以轻易控制和模拟被测试单元的行为方面
方法
TDD看起来挺美好,但在现阶段国内的唯快不破商业环境下,代码迭代频繁,使用起来还是有难度的
单元测试的复杂性
- 输入参数的复杂性:输入参数不是简单的函数输入参数,本质上讲,任何能够影响代码执行路径的参数,都是被测函数的输入参数
- 预期输出的复杂性:主要表现在预期输出应该包括被测函数执行完成后所改写的所有数据
- 关联依赖:需要采用桩代码来模拟不可用的代码,并通过打桩补齐未定义部分
单元测试代码规范
整洁的测试代码
- 可读性
构造-操作-检验
通过不断重构测试就会慢慢得到一个文档的测试API 随着重构代码也会更有表现力
assertUserExistsInDatabase("cxk"); // 一个测试API
对于测试环境 CPU内存等资源没有那么紧张 所以测试也并非一定要追求效率
每个测试应拥有尽可能少的断言 个人认为 可以对断言进行抽象 如上述的测试API
同时 测试应只测试一个概念 将多件事混杂在一起只会导致概念的混乱
@Test
void testUserService(){...} // bad 过于混杂
@Test
void testUserInsertFailed(){...} // better
命名:不要将太多描述放到测试函数命名中,应该放到函数的注释中
断言数量最小化
不要迷信过高覆盖率
单元测试的结构
- 准备阶段(Given) 主要负责创建测试数据、构造mock 方法的返回值,准备环节的编码是单元测试最复杂的部分。需要注意的是 Mockito 库中以 when 开头的函数其实是在准备阶段
- 执行阶段(When) 一般只是调用测试的函数,此部分代码通常较短
- 验证阶段(Then) 通常验证测试函数的执行的结果、 准备阶段 mock 函数的调用次数等是否符合预期
可测的代码
不可测的原因
- 依赖了外部接口组件
- 外部接口组件没有返回期望的返回数据
- 执行外部接口组件会产生副作用
可测改造
对于外部接口组件,使用依赖注入或者通过外部传参的方式来使用,这样利于在测试时进行接缝
- 对象接缝:通过继承 DOC 来改变默认行为
- 接口接缝:将 DOC 提取为接口,并用其他实现类来改变默认行为
- 新生:对于老代码,使用一个新添加的方法来完成新需求,使得新代码可测
- 包装:老代码不动,新代码通过包装调用老代码并添加额外职责的方式完成需求
单元测试覆盖率
粗粒度覆盖
- 方法覆盖与类覆盖
细粒度覆盖
行覆盖
执行的语句总数 / 全部语句总数
分支覆盖
- 确保判定条件的真假都会被执行
if (condition) {...} // condition 为真为假的时候都要测试
条件判定覆盖
- 让判定条件中的语句真假都会被执行
if (condition1 && condition2) {...} // condtion1 和 condtion2 为真为假的组合 也就是要4种组合
条件组合覆盖
- 所有参数的可能取值
路径覆盖
- 测试所有可能的路径
覆盖率工具
- jacoco 通过ASM修改字节码实现
构造数据
- 手动
- 半自动
- 依赖开发工具插件或者外部数据源
- 自动
- java-faker 和 easy-random
java-faker
能生成具体有意义的字符串
easy-random
easy-random 可以轻松构造复杂对象,支持定义对象中集合长度,字符串长度范围,生成集合等
对哪些代码写单测
- 数据访问层
一般要设置自动回滚。除此之外,还可以整合H2等内存数据库来对数据访问层代码进行测试
- 服务层
一般要依赖 mock 工具,将服务的所有依赖都 mock 掉
- 工具类
因为工具类一般在服务内共用,如果有 BUG,影响面很大,很容易造成线上问题或故障。一般需要构造正常和边界值两种类型的用例,对工具类进行全面的测试,才可放心使用