【发布时间】:2021-12-03 01:00:56
【问题描述】:
主要问题:有没有办法在 Spring 的整个上下文中用模拟对象替换 bean 并将确切的 bean 注入测试以验证方法调用?
我有一个 Spring Boot 应用程序,我正在尝试编写一些集成测试,其中我使用 MockMvc 调用 Rest API。
使用Testcontainer 和Localstack 针对实际数据库和AWS 资源运行集成测试。但为了测试与Keycloak 集成为外部依赖的API,我决定模拟KeycloakService 并验证是否将正确的参数传递给该类的正确函数。
我所有的集成测试类都是名为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;
}
考虑有一个类似以下类的子类:
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
}
}
这是根据事件调用KeycloakService函数的类:
@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());
}
}
每当我运行测试时,我都会收到以下错误:
Wanted but not invoked:
keycloakService bean.changeUserStatus(
"rodolfo.kautzer@example.com",
true
);
Actually, there were zero interactions with this mock.
调试代码后,我了解到UserIntegrationTest 中模拟的bean 与注入UserEventSQSListener 类的bean 不同,原因是上下文重新加载。因此,我尝试了其他解决方案,例如使用 Mockito.mock() 创建一个模拟对象并将其作为 bean 返回,以及使用 @MockInBean,但它们都没有奏效。
@TestConfiguration
public static class TestBeanConfig {
@Bean
@Primary
public KeycloakService keycloakService() {
KeycloakService keycloakService = Mockito.mock(KeycloakService.class);
return keycloakService;
}
}
更新 1:
根据@Maziz 的回答,出于调试目的,我将代码更改如下:
@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
}
如您所见,模拟在 UserEventSQSListener 类中被适当地替换:
我仍然收到以下错误:
Wanted but not invoked:
keycloakService.changeUserStatus(
<any string>,
<any boolean>
);
Actually, there were zero interactions with this mock.
【问题讨论】:
-
您确定您的集成测试的 LocalStack SQS 设置正在工作并且事件到达您的侦听器?
-
是的。我愿意。一开始我也是这么想的,但是如果我注入真正的
KeycloakServices bean,更改将应用在 Keycloak 端。 -
如果 keycloakService 模拟 bean 没有被注入,实际的 bean 是否正在初始化并调用实际的服务?
-
我没有阅读整个问题,但是
@TestConfiguration带有覆盖选项对你不起作用? -
由于我测试了不同的解决方案,我错过了赛道。尽管如此,在某些情况下,实际的 bean 会被初始化并注入到
UserEventSQSListener,因此更改会应用于 Keycloak。在某些情况下,UserEventSQSListener会再次初始化,并使用构造函数将新的模拟对象注入其中,但测试类中的引用不会改变。因此将调用另一个模拟对象,并且将在另一个模拟对象上进行验证并抛出错误。 @ParthManaktala
标签: java spring spring-boot mockito spring-test