[英]Mocking external dependencies in Spring Boot integration tests
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?主要问题:有没有办法在 Spring 的整个上下文中用模拟对象替换 bean 并将确切的 bean 注入测试以验证方法调用?
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
.我有一个 Spring Boot 应用程序,我正在尝试编写一些集成测试,在这些测试中我使用
MockMvc
调用 Rest API。
Integration tests run against an actual database and AWS resources using Testcontainer
and Localstack
.使用
Testcontainer
和Localstack
对实际数据库和 AWS 资源运行集成测试。 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.但是为了测试作为外部依赖与
Keycloak
集成的 API,我决定模拟KeycloakService
并验证是否将正确的参数传递给此类的正确函数。
All of my integration test classes are a subclass of an abstract class called AbstractSpringIntegrationTest
:我所有的集成测试类都是名为
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:这是基于事件调用
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());
}
}
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.调试代码后,我了解到
UserIntegrationTest
中UserIntegrationTest
的 bean 与注入到UserEventSQSListener
类中的 bean 不同,因为上下文重新加载。 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.因此,我尝试了其他解决方案,例如使用
Mockito.mock()
创建模拟对象并将其作为 bean 返回,并使用@MockInBean ,但它们效果不佳。
@TestConfiguration
public static class TestBeanConfig {
@Bean
@Primary
public KeycloakService keycloakService() {
KeycloakService keycloakService = Mockito.mock(KeycloakService.class);
return keycloakService;
}
}
Update 1:更新 1:
Based on @Maziz's answer and for debug purposes I change the code like the following:基于@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
}
As you see the mock is appropriately replaced inside the UserEventSQSListener
class:如您所见,模拟在
UserEventSQSListener
类中被适当替换:
Still, I got the following error:尽管如此,我还是收到了以下错误:
Wanted but not invoked:
keycloakService.changeUserStatus(
<any string>,
<any boolean>
);
Actually, there were zero interactions with this mock.
Based on the answer from Maziz, shouldn't the line setField be before the mvc call?根据 Maziz 的回答,行 setField 不应该在 mvc 调用之前吗?
@ParameterizedTest
@ValueSource(booleans = {true, false})
void changeUserStatus_shouldEnableOrDisableTheUser(boolean enabled) throws Exception {
// Some test setup here
ChangeUserStatusRequest request = new ChangeUserStatusRequest()
.setEnabled(enabled);
ReflectionTestUtils.setField(userEventSQSListener, "keycloakService", keycloakService);
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
assertThat(userEventSQSListener.getKeycloakService()).isEqualTo(keycloakService);
await().atMost(10, SECONDS)
.untilAsserted(() -> verify(keycloakService).changeUserStatus(anyString(), anyBoolean())); // Fails to verify method call
}
and if this still does not work you can replace that line with如果这仍然不起作用,您可以用
org.powermock.reflect.Whitebox.setInternalState(UserEventSQSListener.class, "keycloakService", keycloakService);
but the general idea remains the same.但总体思路保持不变。
Did you debug the KeyClockService in the UserEventSQSListener?您是否在 UserEventSQSListener 中调试了 KeyClockService? Did you see if the object is type proxy something indicating a mock object?
您是否看到该对象是否是指示模拟对象的类型代理?
Regardless of the answer, before you call mockMvc.perform, can use不管答案如何,在调用mockMvc.perform之前,可以使用
ReflectionTestUtils.setField(UserEventSQSListener, "keycloakService", keycloakService / the mock object /) ReflectionTestUtils.setField(UserEventSQSListener, "keycloakService", keycloakService /模拟对象/)
Run again.再次运行。 Let me know if it's ok or not.
让我知道它是否可以。
After doing more debugs and thanks to @Maziz's answer, I found out the injection part is working okay, and it's not the issue.在进行了更多调试并感谢@Maziz 的回答后,我发现注入部分工作正常,这不是问题。 The main problem was that I was creating AWS queues in the setup section of tests.
主要问题是我在测试的设置部分创建了 AWS 队列。 That's why the
@SqsListener
doesn't work and doesn't receive any event.这就是
@SqsListener
不起作用并且不接收任何事件的原因。 So, the mocked service will not be called.因此,不会调用模拟服务。
I fixed this by simply moving the queue creation to the context creation of the SQS client, which happens before the message container is created.我通过简单地将队列创建移动到 SQS 客户端的上下文创建来解决这个问题,这发生在创建消息容器之前。
@Bean
@Primary
public AmazonSQSAsync amazonSQS() {
AmazonSQSAsync amazonSQS = AmazonSQSAsyncClientBuilder.standard()
.withCredentials(localStack.getDefaultCredentialsProvider())
.withEndpointConfiguration(localStack.getEndpointConfiguration(LocalStackContainer.Service.SQS))
.build();
createQueues(amazonSQS);
return amazonSQS;
}
private void createQueues(AmazonSQSAsync amazonSQS) {
amazonSQS.createQueue(userRegisteredQueue);
amazonSQS.createQueue(userInvitedQueue);
amazonSQS.createQueue(userStatusChangedQueue);
}
More information about this issue: https://github.com/spring-cloud/spring-cloud-aws/issues/334有关此问题的更多信息: https : //github.com/spring-cloud/spring-cloud-aws/issues/334
我认为这可能与部分上下文刷新有关(尤其是使用@SqsListener
),因此请尝试将此注释放入UserIntegrationTest
:
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.