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.