异常可测需先暴露异常,明确传播路径并显式声明;再按类型精准模拟,覆盖输入校验、外部依赖与多阶段异常;最后验证异常后处理,并隔离还原以防污染。

让异常可测:从代码结构开始
异常要能被测试,前提是它得“露出来”。如果异常被层层吞掉、静默忽略,或者只打日志不抛出,Mock 就无从下手。可测试性设计的第一步,是明确异常的传播路径:哪些方法会抛出检查型异常(如 IOException),哪些非检查型异常(如 IllegalArgumentException)会被主动 throw,哪些异常会被 catch 后包装再抛(如封装为自定义业务异常)。建议在关键服务方法上用 @Throws(Kotlin)或 throws 声明(Java)显式标注,这既是契约,也方便 Mock 工具识别目标点。
精准模拟:按异常类型和触发条件配置 Mock
不是所有异常都该用同一个方式模拟。关键看它在业务逻辑中扮演的角色:
- 输入校验异常:比如参数为空、格式错误。适合用 thenThrow(new IllegalArgumentException("xxx"))(Mockito)或 every { } throws IllegalArgumentException("xxx")(MockK),直接在被调用方法入口处拦截
- 外部依赖异常:如数据库超时、HTTP 连接失败。应 mock 具体的底层组件(如 Mapper、FeignClient、RestTemplate),而不是业务 Service 本身,确保异常发生在真实调用链位置
- 多阶段异常:第一次调用成功、第二次抛异常。用 thenThrow().thenReturn() 或 throwsMany() 实现顺序行为,覆盖重试、降级等逻辑
覆盖“异常后处理”:不止验证抛出,更要验证响应
测试异常不能只停留在“有没有抛”,而要看系统怎么应对。例如一个删除接口,在 DAO 层抛出 SQLException 后,Service 是否返回了统一错误码?是否记录了关键日志?是否触发了补偿动作?这时需结合多种手段:
- 用 assertThrows
{ ... } 验证异常类型和消息 - 用 verify(logger).error(anyString(), eq(exception)) 检查日志是否记录
- 对被测类中私有状态(如重试次数、缓存标记)用 ReflectionTestUtils.getField() 或 spy + doReturn() 辅助断言
- 若异常触发异步动作(如发 MQ 消息),mock 对应消息模板并 verify 发送行为
避免测试污染:隔离与还原是底线
Mock 异常容易影响其他测试用例,尤其当共享对象或静态字段被修改时:
- 优先使用 @BeforeEach 创建全新 mock 实例,而非复用全局 mock
- 修改私有字段(如 ReflectionTestUtils.setField(service, "timeoutMs", 100))后,应在 @AfterEach 中还原原始值或清空
- 对静态方法或 final 类的异常模拟,谨慎使用 Mockito.mockStatic(),并在测试结束时调用 close() 释放资源
- 协程测试中,用 runTest 确保异常在正确调度器中抛出,避免因线程切换导致断言失效











