简体   繁体   English

在 Spring Boot 集成测试中模拟外部依赖项

[英]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 .使用TestcontainerLocalstack对实际数据库和 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.调试代码后,我了解到UserIntegrationTestUserIntegrationTest的 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM