@WebMvcTest를 사용하지 않는 컨트롤러 테스트 작성하기

2024. 7. 29. 12:11·Test

보통 Web Layer(컨트롤러 계층)을 테스트할 때 슬라이싱 테스트인 @WebMvcTest를 사용합니다.

 

이번 글에서는 @WebMvcTest를 사용했을 때 불편했던 점들과 개선한 방법에 대한 경험을 공유하고자 합니다.

@WebMvcTest란?


여러 스프링 테스트 애노테이션 중, Web(Spring MVC)에 집중할 수 있는 애노테이션입니다.
선언할 경우 @Controller, @ControllerAdvice 등을 사용할 수 있습니다.
단, @Service, @Component, @Repository 등은 사용할 수 없습니다. - 스프링 부트와 AWS로 혼자 구현하는 웹 서비스

컨트롤러 계층 만을 슬라이스 테스트할 수 있도록 도와주는 애노테이션입니다.

 

WebMvcTest (Spring Boot 3.1.2 API)

Annotation that can be used for a Spring MVC test that focuses only on Spring MVC components. Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. @Controller, @ControllerAdvice, @JsonC

docs.spring.io

이 애노테이션을 사용하면 전체 자동 구성이 비활성화되고 대신 MVC 테스트와 관련된 구성만 적용됩니다.

@WebMvcTest는 다음과 같이 웹 계층과 관련된 빈들만 찾아서 빈으로 등록합니다. (@Component, @Service, @Repository 제외)

  • @Controller, @RestController
  • @ControllerAdvice, @RestControllerAdvice
  • @JsonComponent
  • Converter/GenericConverter
  • Filter
  • WebMvcConfigurer
  • HandlerMethodArgumentResolver

이런 이유로 @WebMvcTest를 사용해 컨트롤러를 테스트하기 위해서는 부가 장치가 필요합니다. 컨트롤러 빈을 만들기 위해서는 @Service와 같이 의존하는 다른 빈이 필요한데, 해당 빈은 스캔되지 않아 없기 때문입니다. 그래서 @MockBean 또는 @SpyBean을 사용해 가짜 객체를 빈으로 등록해주어야 문제가 발생하지 않습니다.

공식문서에서는, 일반적으로 @Controller 빈에 필요한 다른 빈을 생성하기 위해 @MockBean 또는 @Import를 사용한다고 나와있습니다.

@WebMvcTest를 사용했을 때의 단점들


1. 프로덕션 코드에 의해 영향을 받는 테스트

처음에는 @WebMvcTest와 @MockBean을 사용할 때 컨텍스트 재사용을 위한 추상 클래스를 작성해서, 컨트롤러 테스트가 해당 추상 클래스를 상속받는 형식으로 구현했습니다.

@WebMvcTest(controllers = {
    SomeController1.class,
    SomeController2.class,
    SomeController3.class
    // ... 컨트롤러 계속 추가 ...
})
public abstract class ControllerTestSupporter {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    protected ObjectMapper objectMapper;

    @MockBean
    protected SomeService1 someService1;

    @MockBean
    protected SomeService2 someService2;

    @MockBean
    protected SomeService3 someService3;

    // ... MockBean 계속 추가 ...
}

이후 프로덕션 코드의 개발이 진행되었고, 테스트를 돌려보니 원래 성공했던 테스트가 실패한 것을 확인할 수 있었습니다.

테스트 코드는 변경한 적이 없는데 성공했던 테스트가 실패했다

그 이유는 컨트롤러에서 새로운 @Service 빈을 의존하는 것으로 프로덕션 코드의 변경이 있었기 때문입니다.

 

이러한 원인으로 테스트가 깨지는 것은 이상하다고 생각했습니다.

 

테스트가 성공하도록 만들기 위해 매번 Controller와 @MockBean을 추상 클래스에 함께 추가해줘야만 했고, 이러한 작업이 누락될 가능성이 충분히 존재했습니다.

 

팀원에게도 이 사실을 알려주었고, 이때부터 컨트롤러 테스트의 구조 변경에 대해서 고민하기 시작했습니다.

2. 느려지는 테스트

F.I.R.S.T 원칙을 생각해보면, 테스트는 빨라야 합니다. 하지만, 컨트롤러 테스트를 작성하다 보면 테스트가 점점 느려짐을 느꼈습니다.

그 이유는 @MockBean과 @SpyBean 등이 새로운 컨텍스트를 생성하기 때문입니다.

 

스프링 부트가 제공하는 테스트는 모두 애플리케이션 컨텍스트를 구성합니다. 하지만 모든 테스트마다 이를 구성하려면 비용이 커지므로 스프링은 내부적으로 애플리케이션 컨텍스트를 캐싱해두고 동일한 설정이라면 재사용합니다. 그래서 다음과 같이 애플리케이션 컨텍스트 내부에 변경을 주는 기능들은 새로운 컨텍스트를 생성하도록 요구합니다.

  • @MockBean, @SpyBean
  • @TestPropertySource
  • @ConditionalOnX
  • @WebMvcTest에 컨트롤러 지정
  • @Import
  • 기타

@MockBean과 @SpyBean은 특정 빈을 Mock이 적용된 빈으로 등록합니다. 그러므로 애플리케이션 컨텍스트가 갖는 빈이 달라져 새로운 컨텍스트를 생성하게 됩니다. 그러다보니 @MockBean과 @SpyBean을 많이 사용하면 테스트가 느려질 수 있고 캐싱된 애플리케이션 컨텍스트의 수를 증가시킵니다.

 

일반적으로 컨트롤러 1개당 1개의 @WebMvcTest를 구현하므로 N개의 테스트 컨텍스트에 대한 생성 시간과 비용이 요구됩니다. 그 외에도 @SpringBootTest나 @DataJpaTest 등 스프링 부트가 제공하는 어노테이션을 이용한다면 추가적인 애플리케이션 컨텍스트가 생성될 것이고, 이는 결국 테스트를 느리게 만듭니다.

 

@MockBean과 @SpyBean에 의해 애플리케이션 컨텍스트가 리로딩되는 상황은 이슈로 등록된 적이 있습니다.

하지만 스프링 부트 개발자 philwebb은 두 컨텍스트가 다른 빈을 가지므로, 문제가 아니라고 설명하였습니다.

By philwebb

Spring 테스트 프레임워크는 테스트 실행 사이에 가능할 때마다 ApplicationContext를 캐시합니다. 캐시되려면 컨텍스트가 정확히 동일한 구성을 가져야 합니다. @MockBean을 사용할 때마다 정의상 context configuration을 변경하게 됩니다.

또한, @MockBean과 @SpyBean을 사용했을 때 좋은 디자인이 아님에도 테스트 코드 작성하기가 편해진다는 단점도 존재합니다.

 

@SpyBean @MockBean 의도적으로 사용하지 않기

보통 스프링 부트 관련 테스트 코드를 작성할때 @MockBean과 @SpyBean 를 사용했습니다. (참고: SpringBoot @MockBean, @SpyBean 소개) 복잡한 스프링 프로젝트에서도 원하는 코드만 아주 간단하게 Mock 처리를

jojoldu.tistory.com

개선 방향


구조적인 문제를 해결하면서 테스트 속도를 향상시킬 수 있는 방법이 있을까 고민하던 도중 인프런 질문 답변을 통해 힌트를 얻을 수 있었습니다.

    @Test
    @DisplayName("게시글 요청이 정상적으로 진행된다.")
    void test1() {
        // given
        PostResponse response = PostResponse.builder()
                .title("good")
                .build(); // 성공케이스 응답객체 생성

        PostService postService = mock(PostService.class);
        given(postService.get(1L)).willReturn(response); // stubbing

        PostController controller = new PostController(postService);

        // when
        PostResponse result = controller.get(1L);

        // then
        Assertions.assertEquals(result, response);
    }

단위테스트로 진행해서, 컨트롤러 객체를 생성하는 형태로 테스트를 작성한 것을 확인할 수 있었습니다.

공식문서에서 관련 내용을 찾아본 결과 MockMvc를 standaloneSetup으로 설정해주면 가능할 것 같았습니다.

MockMvc는 웹 API를 테스트할 때 사용합니다.
스프링 MVC 테스트의 시작점입니다.
이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있습니다.
 

MockMvcBuilders (Spring Framework 6.0.11 API)

webAppContextSetup

docs.spring.io

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/setup/MockMvcBuilders.html

하나 이상의 @Controller 인스턴스를 등록하고 Spring MVC 인프라를 프로그래밍 방식으로 구성하여 MockMvc 인스턴스를 빌드합니다. 이를 통해 일반 단위 테스트와 유사하게 컨트롤러 및 해당 종속성의 인스턴스화 및 초기화를 완벽하게 제어할 수 있으며, 한 번에 하나의 컨트롤러를 테스트할 수도 있습니다.

 

따라서 다음과 같이 설정했습니다.

컨트롤러 테스트 시, 다음과 같이 컨트롤러 객체를 생성하는 형태로 테스트를 진행할 수 있습니다.

standaloneSetup을 통해 개선된 방식으로 스프링 컨텍스트를 로딩하는 과정을 스킵했기 때문에 체감상으로도 테스트 속도가 매우 빨라짐을 느낄 수 있었습니다.

저작자표시 (새창열림)

'Test' 카테고리의 다른 글

JUnit 만들어보기⛳️  (0) 2024.12.12
'Test' 카테고리의 다른 글
  • JUnit 만들어보기⛳️
keep it real
keep it real
  • keep it real
    나의 개발일지
    keep it real
  • 전체
    오늘
    어제
    • 분류 전체보기 (5)
      • Java (1)
      • DDD (0)
      • JPA (0)
      • Spring (0)
      • Database (0)
      • CS (0)
      • Test (2)
      • 회고 (1)
      • 트러블 슈팅 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    REDIS
    Database
    Spin Lock
    JPA
    lock
    acid
    Distributed Lock
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
keep it real
@WebMvcTest를 사용하지 않는 컨트롤러 테스트 작성하기
상단으로

티스토리툴바