You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中如何使用Mockito模拟Keycloak框架方法编写单元测试

Keycloak 接口Mockito单元测试正确实现方案

你之前写的链式Mock语句不可行,核心原因是Mockito默认不会自动处理链式调用中间的返回对象:执行keycloak.realm(Mockito.anyString())时,因为没有提前给该方法打桩,默认返回null,后续调用null的.users()方法会直接抛出空指针,根本无法完成后续打桩逻辑。


实现说明

Keycloak的客户端调用属于多级嵌套链式调用,每一级方法都会返回新的资源对象,有两种可行的Mock方案:

  • 逐层打桩(推荐):对每一层返回的资源对象单独Mock,可调试性强,和真实调用逻辑一致
  • 深桩模式(简化用):开启Mockito的深桩配置,直接写链式打桩,适合简单场景,排查问题难度高

方案1:逐层打桩(推荐生产测试用)

第一步:初始化Mock对象

提前声明所有链式调用中会用到的中间资源Mock:

// 测试类基础Mock配置
@Mock
private Keycloak keycloak;
@Mock
private RealmResource realmResource;
@Mock
private UsersResource usersResource;
@Mock
private UserResource userResource;
@Mock
private RealmsResource realmsResource;
@Mock
private ClientsResource clientsResource;
@Mock
private ClientResource clientResource;

// 注入你自己的Keycloak业务服务类
@InjectMocks
private YourKeycloakService keycloakService;

// 测试用固定值
private final String testRealm = "test-realm";
private final String testUserId = "test-user-001";
private final String testClientId = "test-client-001";

第二步:用户详情接口测试用例

@Test
void getUsers_ShouldReturnUserModel_WhenUserIdExists() {
    // 构造模拟的Keycloak返回对象
    UserRepresentation mockUserRep = new UserRepresentation();
    mockUserRep.setId(testUserId);
    mockUserRep.setUsername("testuser");
    mockUserRep.setEmail("test@example.com");
    mockUserRep.setFirstName("Test");
    mockUserRep.setLastName("User");
    mockUserRep.setEnabled(true);
    long createTime = System.currentTimeMillis();
    mockUserRep.setCreatedTimestamp(createTime);
    mockUserRep.setGroups(List.of("default-group"));
    mockUserRep.setRealmRoles(List.of("default-role"));

    // 逐层打桩调用链
    Mockito.when(keycloak.realm(testRealm)).thenReturn(realmResource);
    Mockito.when(realmResource.users()).thenReturn(usersResource);
    Mockito.when(usersResource.get(testUserId)).thenReturn(userResource);
    Mockito.when(userResource.toRepresentation()).thenReturn(mockUserRep);

    // 执行业务方法+断言
    Optional<UserModel> result = keycloakService.getUsers(testUserId);
    Assertions.assertTrue(result.isPresent());
    UserModel user = result.get();
    Assertions.assertEquals(testUserId, user.getId());
    Assertions.assertEquals("testuser", user.getUsername());
    Assertions.assertEquals("test@example.com", user.getEmail());
}

// 空返回场景测试
@Test
void getUsers_ShouldReturnEmpty_WhenUserNotExists() {
    Mockito.when(keycloak.realm(testRealm)).thenReturn(realmResource);
    Mockito.when(realmResource.users()).thenReturn(usersResource);
    Mockito.when(usersResource.get(Mockito.anyString())).thenReturn(null);

    Optional<UserModel> result = keycloakService.getUsers("invalid-id");
    Assertions.assertTrue(result.isEmpty());
}

第三步:Realm详情接口测试用例

@Test
void getRealm_ShouldReturnRealmModel_WhenRealmIdExists() {
    RealmRepresentation mockRealmRep = new RealmRepresentation();
    mockRealmRep.setId("realm-001");
    mockRealmRep.setDisplayName("测试Realm");

    Mockito.when(keycloak.realms()).thenReturn(realmsResource);
    Mockito.when(realmsResource.realm("realm-001")).thenReturn(realmResource);
    Mockito.when(realmResource.toRepresentation()).thenReturn(mockRealmRep);

    Optional<RealmModel> result = keycloakService.getRealm("realm-001");
    Assertions.assertTrue(result.isPresent());
    Assertions.assertEquals("测试Realm", result.get().getDisplayName());
}

第四步:客户端详情接口测试用例

@Test
void getClient_ShouldReturnClientModel_WhenClientIdExists() {
    ClientRepresentation mockClientRep = new ClientRepresentation();
    mockClientRep.setClientId(testClientId);
    mockClientRep.setName("测试客户端");

    Mockito.when(keycloak.realm(testRealm)).thenReturn(realmResource);
    Mockito.when(realmResource.clients()).thenReturn(clientsResource);
    Mockito.when(clientsResource.get(testClientId)).thenReturn(clientResource);
    Mockito.when(clientResource.toRepresentation()).thenReturn(mockClientRep);

    Optional<ClientModel> result = keycloakService.getClient(testClientId);
    Assertions.assertTrue(result.isPresent());
    Assertions.assertEquals("测试客户端", result.get().getName());
}

方案2:深桩模式(简单场景快速实现)

如果不想逐层声明中间Mock对象,可以在初始化Keycloak Mock时开启深桩返回策略,直接支持链式打桩:

// 初始化时指定深桩Answer
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Keycloak keycloak;

// 测试用例里可以直接写链式打桩,不需要单独Mock RealmResource、UsersResource等中间对象
@Test
void getUsers_WithDeepStub() {
    UserRepresentation mockUserRep = new UserRepresentation();
    // 省略mockUserRep属性赋值逻辑
    Mockito.when(keycloak.realm(testRealm).users().get(testUserId)).thenReturn(userResource);
    Mockito.when(userResource.toRepresentation()).thenReturn(mockUserRep);

    // 后续业务调用和断言逻辑和逐层打桩一致
}

注意:深桩模式生成的Mock对象和真实Keycloak客户端行为偏差更大,调用链复杂时出现问题排查成本高,核心业务逻辑优先使用逐层打桩方案。


内容的提问来源于stack exchange,提问作者Kavi Chinna

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 00:31:02