Main question: Is there any way to replace a bean with a mock object in the whole context of Spring and inject the exact bean to the test to verify method calls?
I have a Spring Boot application, and I'm trying to write some integration tests in which I'm calling the Rest APIs using MockMvc.
Integration tests run against an actual database and AWS resources using Testcontainer and Localstack. But for testing APIs which are integrated with Keycloak as an external dependency, I decided to mock KeycloakService and verify that the correct parameters are passed to the proper function of this class.
All of my integration test classes are a subclass of an abstract class called AbstractSpringIntegrationTest:
@Transactional
@Testcontainers
@ActiveProfiles("it")
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@ContextConfiguration(initializers = PostgresITConfig.DockerPostgreDataSourceInitializer.class, classes = {AwsITConfig.class})
public class AbstractSpringIntegrationTest {
@Autowired
public MockMvc mockMvc;
@Autowired
public AmazonSQSAsync amazonSQS;
}
Consider there is a subclass like the following class:
class UserIntegrationTest extends AbstractSpringIntegrationTest {
private static final String USERS_BASE_URL = "/users";
@Autowired
private UserRepository userRepository;
@MockBean
private KeycloakService keycloakService;
@ParameterizedTest
@ValueSource(booleans = {true, false})
void changeUserStatus_shouldEnableOrDisableTheUser(boolean enabled) throws Exception {
// Some test setup here
ChangeUserStatusRequest request = new ChangeUserStatusRequest()
.setEnabled(enabled);
String responseString = mockMvc.perform(patch(USERS_BASE_URL + "/{id}/status", id)
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
// Some assertions here
Awaitility.await()
.atMost(10, SECONDS)
.untilAsserted(() -> verify(keycloakService, times(1)).changeUserStatus(email, enabled); // Fails to verify method call
}
}
And this is the class that calls functions of the KeycloakService based on events:
@Slf4j
@Component
public class UserEventSQSListener {
private final KeycloakService keycloakService;
public UserEventSQSListener(KeycloakService keycloakService) {
this.keycloakService = keycloakService;
}
@SqsListener(value = "${cloud.aws.sqs.user-status-changed-queue}", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void handleUserStatusChangedEvent(UserStatusChangedEvent event) {
keycloakService.changeUserStatus(event.getEmail(), event.isEnabled());
}
}
Whenever I run the test, I get the following error:
Wanted but not invoked:
keycloakService bean.changeUserStatus(
"rodolfo.kautzer@example.com",
true
);
Actually, there were zero interactions with this mock.
After debugging the code, I understood that the bean mocked in UserIntegrationTest is not the same bean injected into the UserEventSQSListener class due to the context reloading. So, I tried other solutions like creating a mock object using Mockito.mock() and return it as a bean, and using @MockInBean, but they didn't work as well.
@TestConfiguration
public static class TestBeanConfig {
@Bean
@Primary
public KeycloakService keycloakService() {
KeycloakService keycloakService = Mockito.mock(KeycloakService.class);
return keycloakService;
}
}
Update 1:
Based on @Maziz's answer and for debug purposes I change the code like the following:
@Component
public class UserEventSQSListener {
private final KeycloakService keycloakService;
public UserEventSQSListener(KeycloakService keycloakService) {
this.keycloakService = keycloakService;
}
public KeycloakService getKeycloakService() {
return keycloakService;
}
...
class UserIT extends AbstractSpringIntegrationTest {
...
@Autowired
private UserEventSQSListener userEventSQSListener;
@Autowired
private Map<String, UserEventSQSListener> beans;
private KeycloakService keycloakService;
@BeforeEach
void setup() {
...
keycloakService = mock(KeycloakService.class);
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void changeUserStatus_shouldEnableOrDisableTheUser(boolean enabled) throws Exception {
// Some test setup here
ChangeUserStatusRequest request = new ChangeUserStatusRequest()
.setEnabled(enabled);
String responseString = mockMvc.perform(patch(USERS_BASE_URL + "/{id}/status", id)
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
// Some assertions here
ReflectionTestUtils.setField(userEventSQSListener, "keycloakService", keycloakService);
assertThat(userEventSQSListener.getKeycloakService()).isEqualTo(keycloakService);
await().atMost(10, SECONDS)
.untilAsserted(() -> verify(keycloakService).changeUserStatus(anyString(), anyBoolean())); // Fails to verify method call
}
As you see the mock is appropriately replaced inside the UserEventSQSListener class:
Still, I got the following error:
Wanted but not invoked:
keycloakService.changeUserStatus(
<any string>,
<any boolean>
);
Actually, there were zero interactions with this mock.
