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

Rez*_*our 6 java spring spring-test mockito spring-boot

主要问题:有没有办法在 Spring 的整个上下文中用模拟对象替换 bean,并将确切的 bean 注入到测试中以验证​​方法调用?

我有一个 Spring Boot 应用程序,我正在尝试编写一些集成测试,其中我使用MockMvc.

Testcontainer使用和针对实际数据库和 AWS 资源运行集成测试Localstack。但为了测试作为外部依赖项集成的 API Keycloak,我决定模拟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;
}
Run Code Online (Sandbox Code Playgroud)

考虑有一个类似以下类的子类:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

这是根据事件调用函数的类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());
    }
}
Run Code Online (Sandbox Code Playgroud)

每当我运行测试时,我都会收到以下错误:

Wanted but not invoked:
keycloakService bean.changeUserStatus(
    "rodolfo.kautzer@example.com",
    true
);

Actually, there were zero interactions with this mock.
Run Code Online (Sandbox Code Playgroud)

调试代码后,我了解到由于上下文重新加载,模拟的 beanUserIntegrationTest与注入到类中的 bean 不同。UserEventSQSListener因此,我尝试了其他解决方案,例如使用创建模拟对象Mockito.mock()并将其作为 bean 返回,以及使用@MockInBean,但它们效果不佳。

    @TestConfiguration
    public static class TestBeanConfig {

        @Bean
        @Primary
        public KeycloakService keycloakService() {
            KeycloakService keycloakService = Mockito.mock(KeycloakService.class);
            return keycloakService;
        }
    }
Run Code Online (Sandbox Code Playgroud)

更新1:

根据@Maziz的回答并出于调试目的,我更改了代码,如下所示:

@Component
public class UserEventSQSListener {

    private final KeycloakService keycloakService;

    public UserEventSQSListener(KeycloakService keycloakService) {
        this.keycloakService = keycloakService;
    }

    public KeycloakService getKeycloakService() {
        return keycloakService;
    }
...
Run Code Online (Sandbox Code Playgroud)
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
    }

Run Code Online (Sandbox Code Playgroud)

正如您所看到的,模拟在UserEventSQSListener类中被适当替换:

调试注入

尽管如此,我还是收到了以下错误:

Wanted but not invoked:
keycloakService.changeUserStatus(
    <any string>,
    <any boolean>
);
Actually, there were zero interactions with this mock.
Run Code Online (Sandbox Code Playgroud)

Maz*_*ziz 1

您是否在 UserEventSQSListener 中调试了 KeyClockService?您是否看到该对象是否是代理类型,表明是模拟对象?

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

ReflectionTestUtils.setField(UserEventSQSListener, "keycloakService", keycloakService /*the mock object*/)
Run Code Online (Sandbox Code Playgroud)

再次运行。让我知道是否可以。