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
相关产品推荐
相关产品推荐

