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 的处理流程如下:

  1. 调用重定向:方法调用通过代理实例被重定向到 Mockito 内部处理机制
  2. 行为匹配:Mockito 检查是否为该方法注册了行为,通过方法签名查找对应的 Answer 实现
  3. 调用信息封装:若找到 Answer 实现,方法参数、签名和模拟对象引用被封装到 InvocationOnMock 实例
  4. 动态行为控制:通过 InvocationOnMockgetArgument(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;
          }
      });
}

关键点解析:

  1. 初始化所有模拟对象
  2. 使用 when(...).thenAnswer(...) 定义动态响应
  3. 根据用户角色和操作类型返回权限结果:
    • Admin 永远返回 true
    • Editor 拒绝删除操作
    • Viewer 仅允许读取
    • Guest 永远返回 false

可通过运行 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);
    }
}

验证逻辑:

  1. 遍历所有操作类型
  2. 断言管理员对所有操作都有权限
  3. 失败时输出具体操作信息

其他角色的验证方法可在代码仓库中查看完整实现。

7. 结论

本文展示了如何使用 Mockito 的 Answer API 在模拟测试中动态实现基于角色的授权逻辑。 通过为用户设置基于角色的访问规则,我们实现了根据参数属性返回不同响应的功能。这种方法能提升代码覆盖率,减少意外故障风险,使测试更可靠高效。

完整实现代码可在 GitHub 仓库 查看。


原始标题:Mockito Answer API: Returning Values Based on Parameters | Baeldung

» 下一篇: Lanterna 介绍