测试Keycloak用户创建时遭遇RESTEASY004645空指针异常
Keycloak用户创建测试空指针异常排查
异常信息
java.lang.NullPointerException: RESTEASY004645: templateValues entry was null at com.springboot.services.KeycloakAdminClientServiceTest.createKeycloakUserTest(KeycloakAdminClientServiceTest.java:80)
问题背景
我正在为AdminClientService编写测试用例,该服务用于向Keycloak数据库创建用户。主业务流程运行正常,但编写测试时触发上述空指针异常。曾尝试用Mockito模拟KeycloakProvider,但未解决问题。
相关代码
测试类代码
@SpringBootTest public class KeycloakAdminClientServiceTest { @Value("${keycloak.auth-server-url}") public String serverUrl; @Value("${keycloak.realm}") public String realm; @Value("${keycloak.resource}") public String clientId; @Value("${keycloak.credentials.secret}") public String clientSecret; // @Mock // KeycloakProvider kcProvider = Mockito.mock(KeycloakProvider.class); // @InjectMocks // // @BeforeEach // void setMockOutput() { // //when(helloService.get()).thenReturn("Hello Mockito"); // Mockito.when(kcProvider.getInstance()).thenReturn(KeycloakBuilder.builder() // .realm(realm) // .serverUrl(serverUrl) // .clientId(clientId) // .clientSecret(clientSecret) // .grantType(OAuth2Constants.CLIENT_CREDENTIALS) // .build()); // } KeycloakProvider kcProvider; KeycloakAdminClientService kcAdminClientService; @BeforeEach void createKeycloakProvider() { kcProvider = new KeycloakProvider(); kcProvider.setServerUrl(serverUrl); kcProvider.setRealm(realm); kcProvider.setClientId(clientId); kcProvider.setClientSecret(clientSecret); kcAdminClientService = new KeycloakAdminClientService(kcProvider); } @Test public void createKeycloakUserTest(){ Integer generatedInteger = (int) ((Math.random() * (9999999 - 1000000)) + 1000000); String generatedString = generatedInteger.toString(); CreateUserRequest createUserRequest = new CreateUserRequest(); createUserRequest.setUsername("admin_client_username_" + generatedString); createUserRequest.setPassword("admin_client_password"); createUserRequest.setEmail("admin_client_email_" + generatedString + "@gmail.com"); createUserRequest.setFirstname("admin_client_firstname_" + generatedString); createUserRequest.setLastname("admin_client_lastname_" + generatedString); kcAdminClientService.createKeycloakUser(createUserRequest); UsersResource usersResource = kcProvider.getInstance() .realm(kcAdminClientService.realm).users(); List<UserRepresentation> userRepresentation = usersResource .search("admin_client_email_" + generatedString + "@gmail.com"); assertNotNull(userRepresentation); assertEquals("admin_client_firstname_" + generatedString, userRepresentation.get(0).getFirstName()); } }
服务类代码
@Service public class KeycloakAdminClientService { @Value("${keycloak.realm}") public String realm; private final KeycloakProvider kcProvider; public KeycloakAdminClientService(KeycloakProvider keycloakProvider) { this.kcProvider = keycloakProvider; } public Response createKeycloakUser(CreateUserRequest user) { UsersResource usersResource = kcProvider.getInstance().realm(realm).users(); CredentialRepresentation credentialRepresentation = createPasswordCredentials( user.getPassword()); UserRepresentation kcUser = new UserRepresentation(); kcUser.setUsername(user.getEmail()); kcUser.setCredentials(Collections.singletonList(credentialRepresentation)); kcUser.setFirstName(user.getFirstname()); kcUser.setLastName(user.getLastname()); kcUser.setEmail(user.getEmail()); kcUser.setEnabled(true); kcUser.setEmailVerified(false); Response response = usersResource.create(kcUser); return response; } private static CredentialRepresentation createPasswordCredentials(String password) { CredentialRepresentation passwordCredentials = new CredentialRepresentation(); passwordCredentials.setTemporary(false); passwordCredentials.setType(CredentialRepresentation.PASSWORD); passwordCredentials.setValue(password); return passwordCredentials; } }
KeycloakProvider代码
@Configuration @Getter @Setter public class KeycloakProvider { @Value("${keycloak.auth-server-url}") public String serverUrl; @Value("${keycloak.realm}") public String realm; @Value("${keycloak.resource}") public String clientId; @Value("${keycloak.credentials.secret}") public String clientSecret; private static Keycloak keycloak = null; public KeycloakProvider() { } public Keycloak getInstance() { if (keycloak == null) { return KeycloakBuilder.builder() .realm(realm) .serverUrl(serverUrl) .clientId(clientId) .clientSecret(clientSecret) .grantType(OAuth2Constants.CLIENT_CREDENTIALS) .build(); } return keycloak; } public KeycloakBuilder newKeycloakBuilderWithPasswordCredentials( String username, String password) { return KeycloakBuilder.builder() .realm(realm) .serverUrl(serverUrl) .clientId(clientId) .clientSecret(clientSecret) .username(username) .password(password); } public JsonNode refreshToken(String refreshToken) throws UnirestException { String url = serverUrl + "/realms/" + realm + "/protocol/openid-connect/token"; return Unirest.post(url) .header("Content-Type", "application/x-www-form-urlencoded") .field("client_id", clientId) .field("client_secret", clientSecret) .field("refresh_token", refreshToken) .field("grant_type", "refresh_token") .asJson().getBody(); } }
异常排查与解决方案
异常原因分析
RESTEASY004645: templateValues entry was null本质是Keycloak客户端构建时传入了null参数,导致RESTEasy在拼接请求URL模板时出错。结合代码来看,大概率是以下两种情况:
- 测试环境配置缺失:
@Value注入的serverUrl/realm/clientId/clientSecret为空(测试配置文件未配置对应属性) - 手动实例化
KeycloakProvider的逻辑问题:静态变量keycloak在多测试场景下状态污染,或参数传递未生效
解决方案
方案1:补全测试环境配置
确保测试环境的配置文件(如application-test.yml)中完整配置Keycloak参数:
keycloak: auth-server-url: http://your-keycloak-url:8080 realm: your-realm-name resource: your-client-id credentials: secret: your-client-secret
方案2:改用Mockito隔离外部依赖(推荐)
单元测试应避免调用真实Keycloak服务,通过Mock完全隔离依赖:
@SpringBootTest public class KeycloakAdminClientServiceTest { @Mock private KeycloakProvider kcProvider; @Mock private Keycloak keycloak; @Mock private RealmResource realmResource; @Mock private UsersResource usersResource; @InjectMocks private KeycloakAdminClientService kcAdminClientService; @Value("${keycloak.realm}") private String realm; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); // 模拟Keycloak调用链 when(kcProvider.getInstance()).thenReturn(keycloak); when(keycloak.realm(realm)).thenReturn(realmResource); when(realmResource.users()).thenReturn(usersResource); // 模拟用户创建成功响应 when(usersResource.create(any(UserRepresentation.class))) .thenReturn(Response.created(URI.create("/users/123")).build()); } @Test public void createKeycloakUserTest(){ Integer generatedInteger = (int) ((Math.random() * (9999999 - 1000000)) + 1000000); String generatedString = generatedInteger.toString(); CreateUserRequest createUserRequest = new CreateUserRequest(); createUserRequest.setUsername("admin_client_username_" + generatedString); createUserRequest.setPassword("admin_client_password"); createUserRequest.setEmail("admin_client_email_" + generatedString + "@gmail.com"); createUserRequest.setFirstname("admin_client_firstname_" + generatedString); createUserRequest.setLastname("admin_client_lastname_" + generatedString); Response response = kcAdminClientService.createKeycloakUser(createUserRequest); // 验证响应状态 assertEquals(HttpStatus.CREATED.value(), response.getStatus()); // 验证用户参数是否正确传递 ArgumentCaptor<UserRepresentation> userCaptor = ArgumentCaptor.forClass(UserRepresentation.class); verify(usersResource).create(userCaptor.capture()); UserRepresentation capturedUser = userCaptor.getValue(); assertEquals(createUserRequest.getEmail(), capturedUser.getUsername()); assertEquals(createUserRequest.getFirstname(), capturedUser.getFirstName()); assertEquals(createUserRequest.getLastname(), capturedUser.getLastName()); assertTrue(capturedUser.isEnabled()); } }
优化建议:调整服务类注入方式
将服务类中realm的@Value注入改为构造函数注入,提升测试灵活性:
@Service public class KeycloakAdminClientService { private final String realm; private final KeycloakProvider kcProvider; public KeycloakAdminClientService(KeycloakProvider keycloakProvider, @Value("${keycloak.realm}") String realm) { this.kcProvider = keycloakProvider; this.realm = realm; } // 其余方法不变... }
内容的提问来源于stack exchange,提问作者MrMRas
相关产品推荐
相关产品推荐

