Spring/testing

멀티 모듈 환경에서 WebTestClient 이용 시 주의 사항

자몽포도 2023. 8. 14. 02:52

객체 내 java docs 설명에 대한 번역을 chatGPT의 도움을 받았습니다.

예를 들면 아래와 같은 주석을 번역기 돌렸다는 말입니다.

 

 

이 문제를 해결하면서 배운 내용도 정리한 글이라 생각보다 장황할 수 있습니다. 문제의 원인/해결을 들어가기 전에 간단하게 요약하겠습니다.

 

원인

한 프로젝트 내 모듈 A / 모듈 B가 존재합니다.

모듈 A spring-web 의존

모듈 B srping-webflux 의존

 

한 프로젝트 내 spring-web/spring-webflux 모두 의존하게 되는 상황

 

해결책

Spring docs - Detecting Web Application Type 를 참고하시면 됩니다.

 

 

 

WebTestClient 


리액티브 환경에서 컨트롤러 레이어의 테스트를 원활하게 수행하기 위해서는 WebTestClient 가 필요합니다.

 

WebTestClient 객체의 역할은 다음과 같습니다.

 

 

요약하면 여튼 필요합니다.

 

제 생각을 조금 더해보자면 

WebFlux는 동기/블락킹 기반의 tomcat WAS 대신 비동기 기반/논블락킹 기반의 netty를 WAS로 사용하기 때문입니다. 이로 인해 필요한 구성요소가 달라집니다. 예를 들어 WebFlux 에서 테스트를 수행하기 위해서는 Mono, Flux 타입과 같은 Publisher 가 필요합니다. 일반적인(tomcat was)를 띄우는 spring-web 프로젝트에서는 해당 객체들이 주입되지 않습니다.

 

@SpringBootTest
@AutoConfigureMockMvc
class PlaceControllerTest {
    @Autowired MockMvc mockMvc;
    @Autowired ObjectMapper objectMapper;

    @Test
    void save_places_success() throws Exception {
        PlaceForm placeForm = new PlaceForm(); 

        mockMvc.perform(post("/places")
                        .contentType(MediaType.APPLICATION_JSON)
                        // 이 부분을 처리할 수 없음
                        .content(objectMapper.writeValueAsString(Mono.just(placeForm)))) 
                .andExpect(jsonPath("$.message").value(ENROLL))
                .andExpect(status().isCreated());
    }

 

정확히는 아래와 같은 에러가 발생합니다.

Request processing failed: org.springframework.http.converter.HttpMessageConversionException: 
Type definition error: [simple type, class reactor.core.publisher.Mono]
jakarta.servlet.ServletException: 
Request processing failed: org.springframework.http.converter.HttpMessageConversionException

 

컨버팅 과정에서 Mono 타입을 처리할 수 없어보입니다. 아마도 이 부분을 해결한다고 해도 또 다른 문제가 기다리고 있지 않을까 싶습니다.

 

결론적으로 WebFlux 환경에서 컨트롤러 레이어를 테스트하려면 WebTestClient 를 주입하는것이 좋아보입니다.

 

 

 

@AutoConfigureWebTestClient, @WebFluxTest


두 애노테이션은 대표적으로 WebTestClient 주입을 제공하는 애노테이션입니다. 

 

 

위 설명에 따르면 @AutoConfigureWebTestClient 은 @SpringBootTest와 함께 사용할 수 있습니다. @SpringBootTest는 잘 아시다시피 ApplicationContext를 띄웁니다. 따라서 우리가 만들 @Bean, @Component 등을 모두 등록합니다.

 

반면 @WebFluxTest는 컨트롤러 레이어 전용 테스트 애노테이션이라고 생각하면 됩니다. 쉽게 @WebMvcTest와 비슷합니다. 쉽게 @Controller, @ControllerAdvice 등을 선언한 빈들을 이용할 수 있습니다. 반면 @Component, @Service, @Repository 빈들은 이용할 수 없습니다. 아래는 이에 대한 java docs 설명입니다.

 

WebFluxTest

 

 

상황에 따라 어떤 애노테이션을 사용할지 판단하면 됩니다. 목적이 컨트롤러 레이어만 테스트하거나 혹여나 API 문서를 만들기 위해 작성하는 테스트 코드에서는 @WebFluxTest가 가벼우니 좋아보인다고 생각합니다.

 

반면 end-to-end 테스트가 필요할 때는 @AutoConfigureWebTestClient 와 @SpringBootTest 를 함께 사용하는게 좋아보입니다. 제게 필요한 것은 end-to-end 테스트였기에 두 애노테이션을 사용했습니다.

 

 

 

멀티 모듈 환경에서 WebClientTest


멀티 모듈이라고 무조건 발생하는건 아닙니다. 멀티 모듈을 구성하는 과정에서 모듈에 의존성을 더하면서 문제가 생긴 것 같습니다.

 

@SpringBootTest
@AutoConfigureWebTestClient
class PlaceControllerTest {
    @Autowired WebTestClient webTestClient;

    @DisplayName("장소 등록 요청 - 성공")
    @Test
    void save_places_success() throws Exception {
        PlaceForm placeForm = new PlaceForm();

        webTestClient.post()
                .uri("/places")
                .contentType(MediaType.APPLICATION_JSON)
                .body(Mono.just(placeForm), PlaceForm.class)

                .exchange()
                .expectStatus().isCreated()
                .expectBody().jsonPath("$.message", ENROLL);
    }

 

 

정확히 아래와 같은 에러가 발생했습니다.

 

 

쉽게 말하면 WebTestClient 후보가 될 수 있는 Bean이 2개 이상이라는 말인 것 같습니다. 위 같은 에러는 아래와 같은 상황에서 발생할 수 있습니다.

 

@Bean
WebTestClient xuniClient() {
    return WebClient.create();
}

@Bean
WebTestClient yuniClient() {
    return WebClient.create();
}

 

동일한 인터페이스에 대해 2개의 빈이 등록되기 때문에 No qualifying 되었다고 합니다. 일반적으로 @Qualify 애노테이션을 통해 주입 단계에서 이를 해결할 수 있습니다. 아래는 생성자 주입을 통해 이 문제를 해결하는 방법입니다.

 

public PlaceController(PlaceService placeService, @Qualifier("xuniClient") WebClient webClient) {
    this.placeService = placeService;
    this.webClient = webClient;
}

 

 

해결 방법은 이러한 접근을 해야되는 것을 아는데요. 중요한 건 WebTestClient라는 객체를 저는 제가 직접 빈으로 등록한 적이 없다고 생각하고 있었습니다. 그래서 이를 어찌할꼬 고민하다가 어부지리로 해결되었습니다.

 

저는 여기서 해결책을 얻었습니다. 

 

위에서 설명하기를 WebFlux, Spring MVC가 둘 다 존재한다면 반드시 spring.main.web-application-type 을 설정해야 한다고 합니다. spring.main.web-application-type 값에는 아래와 같은 값이 들어갈 수 있습니다. 

 

/**
 * The application should not run as a web application and should not start an
 * embedded web server.
 */
NONE,

/**
 * The application should run as a servlet-based web application and should start an
 * embedded servlet web server.
 */
SERVLET,

/**
 * The application should run as a reactive web application and should start an
 * embedded reactive web server.
 */
REACTIVE;

 

위 속성을 서정하면 테스트 레벨에서 애플리케이션을 띄울 때 어떤 WAS가 띄워질지 결정됩니다. REACTIVE 속성을 이용하면 embedded reactive web server 가 실행된다고 합니다.

 

 

더 자세한 내용은 WebApplicationType docs에서 확인하시면 됩니다. 결론적으로 SpringBootTest properties 값을 설정해주면 됩니다.

 

@SpringBootTest(properties = "spring.main.web-application-type=reactive")

 

전체 코드는 아래와 같습니다.

 

@SpringBootTest(properties = "spring.main.web-application-type=reactive")
@AutoConfigureWebTestClient
class PlaceControllerTest {
    @Autowired WebTestClient webTestClient;

    @Test
    void save_places_success() throws Exception {
        PlaceForm placeForm = new PlaceForm();

        webTestClient.post()
                .uri("/places")
                .contentType(MediaType.APPLICATION_JSON)
                .body(Mono.just(placeForm), PlaceForm.class)

                .exchange()
                .expectStatus().isCreated()
                .expectBody().jsonPath("$.message", ENROLL);
    }
 }