1. 引言
Mocking 是一种测试技术,用预设行为的对象替换真实组件。这样开发者可以隔离并测试特定组件,无需依赖外部系统。Mock 对象的核心特征是:它们对方法调用有预设响应,并能验证执行情况。
本教程将通过一个用户角色认证服务的测试案例,展示如何使用 Mockito 的 Answer API 实现基于不同角色的动态响应。
2. Maven 依赖
开始前需添加核心依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
测试框架依赖(JUnit 5):
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
3. Answer API 详解
*Answer API 允许我们自定义模拟方法的行为,根据调用参数动态返回结果。 当需要根据输入参数提供不同响应时,这个特性特别有用。下面逐步拆解其核心机制。
Mockito 的 Answer API 通过拦截模拟对象的方法调用,将请求重定向到自定义逻辑。整个过程无需修改底层代码,就能模拟复杂行为。我们从模拟创建到方法拦截的完整流程展开说明。
3.1 使用 thenAnswer()
创建模拟对象和方法存根
Mockito 通过 mock()
方法创建目标类的代理对象(基于 CGLib 或反射 API),并在内部注册该代理以管理生命周期。
创建模拟对象后,通过方法存根定义行为。Mockito 拦截调用,识别目标方法和参数,使用 thenAnswer()
设置自定义响应:
// 模拟 OrderService 类
OrderService orderService = mock(OrderService.class);
// 存根 processOrder 方法
when(orderService.processOrder(anyString())).thenAnswer(invocation -> {
String orderId = invocation.getArgument(0);
return "Order " + orderId + " processed successfully";
});
// 调用存根方法
String result = orderService.processOrder("12345");
System.out.println(result); // 输出: Order 12345 processed successfully
这里 processOrder()
被存根为返回包含订单 ID 的消息。调用时,Mockito 拦截并应用自定义的 Answer
逻辑。
3.2 方法调用拦截机制
理解 Answer API 的内部流程对灵活测试至关重要。当测试中调用模拟方法时,Mockito 的处理流程如下:
- 调用重定向:方法调用通过代理实例被重定向到 Mockito 内部处理机制
- 行为匹配:Mockito 检查是否为该方法注册了行为,通过方法签名查找对应的
Answer
实现 - 调用信息封装:若找到
Answer
实现,方法参数、签名和模拟对象引用被封装到InvocationOnMock
实例 - 动态行为控制:通过
InvocationOnMock
的getArgument(int index)
访问参数,实现动态行为控制
这种机制使 Answer API 能根据上下文动态响应。例如在内容管理系统中,用户权限因角色而异。我们可以用 Answer API 动态模拟授权逻辑,具体实现见后续章节。
4. 创建用户和动作模型
以内容管理系统为例,我们定义四种角色:Admin、Editor、Viewer 和 Guest。这些角色对应不同的 CRUD 操作权限:
- ✅ Admin:所有操作
- ✅ Editor:创建、读取、更新
- ✅ Viewer:仅读取
- ❌ Guest:无权限
先创建用户类:
public class CmsUser {
private String username;
private String role;
public CmsUser(String username, String role) {
this.username = username;
this.role = role;
}
public String getRole() {
return this.role;
}
}
定义 CRUD 操作枚举:
public enum ActionEnum {
CREATE, READ, UPDATE, DELETE;
}
现在定义服务接口:
public interface AuthorizationService {
boolean authorize(CmsUser user, ActionEnum actionEnum);
}
该方法判断用户是否有权执行指定操作。接下来实现 Answer API 的核心逻辑。
5. 创建授权服务测试
首先创建模拟对象:
@Mock
private AuthorizationService authorizationService;
在初始化方法中定义 authorize()
的动态行为:
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(this.authorizationService.authorize(any(CmsUser.class), any(ActionEnum.class)))
.thenAnswer(invocation -> {
CmsUser user = invocation.getArgument(0);
ActionEnum action = invocation.getArgument(1);
switch(user.getRole()) {
case "ADMIN": return true;
case "EDITOR": return action != ActionEnum.DELETE;
case "VIEWER": return action == ActionEnum.READ;
case "GUEST":
default: return false;
}
});
}
关键点解析:
- 初始化所有模拟对象
- 使用
when(...).thenAnswer(...)
定义动态响应 - 根据用户角色和操作类型返回权限结果:
- Admin 永远返回
true
- Editor 拒绝删除操作
- Viewer 仅允许读取
- Guest 永远返回
false
- Admin 永远返回
可通过运行 givenRoles_whenInvokingAuthorizationService_thenReturnExpectedResults()
验证正确性。
6. 验证实现
创建测试方法验证不同角色的权限:
@Test
public void givenRoles_whenInvokingAuthorizationService_thenReturnExpectedResults() {
CmsUser adminUser = createCmsUser("admin@example.com", "ADMIN");
CmsUser guestUser = createCmsUser("guest@example.com", "GUEST");
CmsUser editorUser = createCmsUser("editor@example.com", "EDITOR");
CmsUser viewerUser = createCmsUser("viewer@example.com", "VIEWER");
verifyAdminUserAccess(adminUser);
verifyEditorUserAccess(editorUser);
verifyViewerUserAccess(viewerUser);
verifyGuestUserAccess(guestUser);
}
以管理员权限验证为例(其他角色逻辑类似):
private void verifyAdminUserAccess(CmsUser adminUser) {
for (ActionEnum action : ActionEnum.values()) {
assertTrue(authorizationService.authorize(adminUser, action),
"Admin should have access to " + action);
}
}
验证逻辑:
- 遍历所有操作类型
- 断言管理员对所有操作都有权限
- 失败时输出具体操作信息
其他角色的验证方法可在代码仓库中查看完整实现。
7. 结论
本文展示了如何使用 Mockito 的 Answer API 在模拟测试中动态实现基于角色的授权逻辑。 通过为用户设置基于角色的访问规则,我们实现了根据参数属性返回不同响应的功能。这种方法能提升代码覆盖率,减少意外故障风险,使测试更可靠高效。
完整实现代码可在 GitHub 仓库 查看。