集成测试Mock测试与依赖隔离安全权限配置实践 - 第2节
2.1 Mock对象定义
Mock对象是测试替身(Test Double)的一种,用于在单元测试或集成测试中模拟真实依赖的行为。它能够记录调用信息(如调用次数、参数、顺序),并根据预设的期望(Expectation)返回指定结果或抛出异常。
核心特征: Mock对象不仅提供“假实现”,还具备行为验证能力——测试结束后可以断言“某个方法是否被调用”“调用了多少次”“传入什么参数”。
| 特性 | 说明 |
|---|---|
| 行为记录 | 自动记录所有方法调用的参数、次数、顺序 |
| 预设返回值 | 可配置当调用某方法时返回固定值或动态计算结果 |
| 异常模拟 | 可配置在特定调用时抛出异常,测试异常处理路径 |
| 验证断言 | 测试结束后可验证是否按预期调用了依赖 |
2.2 Mock与Stub/Fake的区别
三种测试替身常被混淆,但职责和复杂度有明显差异:
| 类型 | 核心目的 | 行为验证 | 实现复杂度 | 典型场景 |
|---|---|---|---|---|
| Stub | 提供预设返回值,驱动被测对象走特定路径 | 无 | 低 | 替代只读数据源、配置服务 |
| Fake | 提供轻量级可工作实现(如内存数据库) | 无 | 中 | 替代数据库、消息队列 |
| Mock | 验证交互行为是否符合预期 | 有 | 高 | 验证外部服务调用、事件发布 |
记忆口诀: Stub问“返回什么”,Fake问“怎么工作”,Mock问“是否按预期调用”。
2.3 Mock框架工作原理
主流Mock框架(Mockito、EasyMock、PowerMock、jMock等)底层依赖两种核心技术:动态代理和字节码增强。
2.3.1 动态代理(Dynamic Proxy)
适用于接口或抽象类。框架在运行时生成一个实现目标接口的代理类,所有方法调用被拦截到InvocationHandler,由框架决定返回预设值还是记录调用。
// Mockito 动态代理示例(接口)
public interface UserService {
User findById(Long id);
}
// 测试中
UserService mock = Mockito.mock(UserService.class);
Mockito.when(mock.findById(1L)).thenReturn(new User("Alice"));
mock.findById(1L);
Mockito.verify(mock).findById(1L); // 验证调用
2.3.2 字节码增强(Bytecode Enhancement)
适用于具体类、final类、静态方法或私有方法。框架(如PowerMock、Mockito inline、ByteBuddy)在类加载时或运行时修改字节码,插入拦截逻辑。
// PowerMock 字节码增强示例(具体类)
public class Logger {
public final void log(String msg) { /* 真实实现 */ }
}
// 测试中
Logger mock = PowerMock.createMock(Logger.class);
PowerMock.expectLastCall().andThrow(new RuntimeException("模拟异常"));
PowerMock.replay(mock);
mock.log("test");
PowerMock.verify(mock);
性能与限制: 字节码增强比动态代理开销更大,且可能被某些安全策略(如SecurityManager)阻止。在集成测试中优先使用动态代理,仅在无法Mock final/static方法时使用字节码增强。
2.4 集成测试中的Mock选择策略
在集成测试中,Mock并非万能。以下决策树可帮助选择正确的替身类型:
- 依赖是否可控? 是 → 使用真实实例或Fake;否 → 进入下一步
- 是否需要验证交互行为? 是 → 使用Mock;否 → 使用Stub
- 依赖是接口还是具体类? 接口 → 动态代理Mock;具体类 → 字节码增强Mock
- 是否涉及静态/私有方法? 是 → 仅PowerMock/ByteBuddy;否 → Mockito inline即可
安全权限提示: 在集成测试中Mock外部服务(如认证中心、权限网关)时,务必确保Mock对象不会绕过安全上下文。建议在Mock中显式注入安全令牌或权限上下文,避免测试环境与生产环境的安全策略不一致。