簡體   English   中英

在 Spring Boot 集成測試中模擬外部依賴項

[英]Mocking external dependencies in Spring Boot integration tests

主要問題:有沒有辦法在 Spring 的整個上下文中用模擬對象替換 bean 並將確切的 bean 注入測試以驗證方法調用?

我有一個 Spring Boot 應用程序,我正在嘗試編寫一些集成測試,在這些測試中我使用MockMvc調用 Rest API。

使用TestcontainerLocalstack對實際數據庫和 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.

調試代碼后,我了解到UserIntegrationTestUserIntegrationTest的 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.

根據 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
    }

如果這仍然不起作用,您可以用

org.powermock.reflect.Whitebox.setInternalState(UserEventSQSListener.class, "keycloakService", keycloakService);

但總體思路保持不變。

您是否在 UserEventSQSListener 中調試了 KeyClockService? 您是否看到該對象是否是指示模擬對象的類型代理?

不管答案如何,在調用mockMvc.perform之前,可以使用

ReflectionTestUtils.setField(UserEventSQSListener, "keycloakService", keycloakService /模擬對象/)

再次運行。 讓我知道它是否可以。

在進行了更多調試並感謝@Maziz 的回答后,我發現注入部分工作正常,這不是問題。 主要問題是我在測試的設置部分創建了 AWS 隊列。 這就是@SqsListener不起作用並且不接收任何事件的原因。 因此,不會調用模擬服務。

我通過簡單地將隊列創建移動到 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);
    }

有關此問題的更多信息: 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