티스토리 뷰
이번에는 @WebMvcTest가 갖는 한계와 이를 극복하기 위해 직접 개발해본 라이브러리를 소개해보고자 합니다. 매우 초기 버전인만큼 어떠한 버그와 문제가 나올지 모르지만, 써보시고 이슈 등록이나 문제점 공유 등 해주시면 감사드리겠습니다!
1. @WebMvcTest에 의해 느려지는 테스트 속도
[ @WebMvcTest에 대하여 ]
스프링부트는 편리한 테스트를 위한 다양한 테스트 어노테이션을 제공하고 있다. 그 중에서 @WebMvcTest는 컨트롤러 계층 만을 슬라이스 테스트할 수 있도록 도와주는 어노테이션이며, 다음과 같이 사용된다.
@WebMvcTest(MemberController.class)
class MemberControllerTest {
@MockBean
private MemberService memberService;
@Autowired
private MockMvc mockMvc;
}
@WebMvcTest는 다음과 같이 웹 계층과 관련된 빈들만을 찾아서 빈으로 등록한다. 일반적인 @Component나 @ConfigurationProperties 등을 사용하는 빈들은 스캔되지 않는다.
- @Controller, @RestController
- @ControllerAdvice, @RestControllerAdvice
- @JsonComponent
- Filter
- WebMvcConfigurer
- HandlerMethodArgumentResolver
이러한 이유로 @WebMvcTest를 사용해 컨트롤러를 테스트하려면 부가 장치가 필요하다. 컨트롤러 빈을 만들기 위해서는 @Service와 같이 의존하는 다른 빈이 필요한데, 해당 빈은 스캔되지 않아 없기 때문이다. 그래서 @MockBean 또는 @SpyBean을 사용해 가짜 객체를 빈으로 등록해주어야 문제가 발생하지 않는다. 그런데 문제는 @MockBean과 @SpyBean이 테스트 성능에 영향을 준다는 것이다.
[ @WebMvcTest에 의해 느려지는 테스트 속도 ]
애플리케이션 컨텍스트 캐싱과 @WebMvcTest의 한계
스프링 부트가 제공하는 테스트는 모두 애플리케이션 컨텍스트를 구성한다. 하지만 모든 테스트마다 이를 구성하려면 비용이 커지므로 스프링은 내부적으로 애플리케이션 컨텍스트를 캐싱해두고 동일한 설정이라면 재사용한다. 그래서 다음과 같이 애플리케이션 컨텍스트 내부에 변경을 주는 기능들은 새로운 컨텍스트를 생성하도록 요구한다.
- @MockBean, @SpyBean
- @TestPropertySource
- @ConditionalOnX
- @WebMvcTest에 컨트롤러 지정
- @Import
- 기타 등등
@MockBean과 @SpyBean은 특정 빈을 Mock이 적용된 빈으로 등록한다. 그러므로 애플리케이션 컨텍스트가 갖는 빈이 달라져 새로운 컨텍스트를 생성하게 된다. 그러다보니 @MockBean과 @SpyBean을 많이 사용하면 테스트가 느려질 수 있고 캐싱된 애플리케이션 컨텍스트의 수를 증가시킨다. 일반적으로 컨트롤러 1개당 1개의 @WebMvcTest를 구현하므로 N개의 테스트 컨텍스트에 대한 생성 시간과 비용이 요구된다. 그 외에도 @SpringBootTest나 @DataJpaTest 등 스프링 부트가 제공하는 어노테이션을 이용한다면 추가적인 애플리케이션 컨텍스트가 생성될 것이고, 이는 결국 테스트를 느리게 만든다.
의도된 상황인 @MockBean과 @SpyBean
@MockBean과 @SpyBean에 의해 애플리케이션 컨텍스트가 리로딩되는 상황은 이미 이슈로 등록된 적이 있었다. 하지만 스프링부트 개발자 philwebb은 두 컨텍스트가 다른 빈을 가지므로, 문제가 아니라고 설명하였다. 하지만 그렇다고 모든 @WebMvcTest마다 스프링 컨텍스트가 만들어지는 것은 바람직하지 않기에 해결 방법을 찾아야 했다.
2. @WebMvcTest의 문제를 해결하는 방법(컨트롤러에 대한 단위 테스트 작성하기)
[ Mockito 기반의 단위 테스트로 작성하기 ]
@WebMvcTest의 한계를 극복하는 가장 심플한 방법은 @WebMvcTest를 사용하지 않고 단위 테스트로 작성하는 것이다. 기존에 @WebMvcTest로 작성된 코드는 다음과 같이 동일하게 구현할 수 있다.
@ExtendWith(MockitoExtension.class)
class MemberControllerTest {
@InjectMocks
private MemberController target;
@Mock
private MemberService memberService;
private MockMvc mockMvc;
@BeforeEach
public void init() {
mockMvc = MockMvcBuilders.standaloneSetup(target)
.build();
}
}
위와 같이 단위 테스트로 작성된 코드는 스프링 컨텍스트를 사용하지 않으므로 테스트 속도가 훨씬 빠르다. 하지만 위와 같이 단위 테스트로 작성하는 코드는 다음과 같은 단점을 갖는다는 것이다.
- MockMvc를 직접 만들어야 함
- @Autowired를 사용할 수 없음
- 새로운 웹 계층 설정이 추가되면 모든 테스트에 반영해주어야 함
1. MockMvc를 직접 만들어야 함
@WebMvcTest를 사용하면 스프링이 MockMvc를 자동으로 만들어준다. 하지만 단위 테스트로 작성하면 MockMvc를 직접 구현해주어야 하는데, 계속 테스트를 작성하다보면 상당히 번거롭다. 이는 단순 반복 작업이기에 자동화를 떠올리지 않을 수 없다.
2. @Autowired를 사용할 수 없음
프로덕션 코드에서 @Autwowired는 바람직하지 않지만 테스트 코드에서는 코드 작성이 간결해져 유용하다. 하지만 단위 테스트를 작성하면 애플리케이션 컨텍스트가 존재하지 않으므로 @Autowired를 사용할 수 없다.
3. 웹 계층 설정 변경 시에 번거로움
단위 테스트에서는 MockMvc를 직접 생성해주는데, 웹 계층과 관련된 객체들도 빈들로 등록하려면 MockMvcBuilders에서 추가적으로 등록해주어야 한다. 만약 계층에 대한 설정이 변경되면 모든 컨트롤러 테스트 코드를 수정해야 하며 상당히 번거로워진다.
예를 들어 예외 처리 방식으로 ControllerAdvice를 새롭게 추가하였다고 하자. 그러면 모든 컨트롤러 테스트 코드를 다음과 같이 변경해주어야 할텐데, 이는 테스트 유지보수 비용을 증가시킨다. 또한 웹 계층과 관련된 모든 객체를 알고 있어야 한다는 점에서도 좋지 않다.
@ExtendWith(MockitoExtension.class)
class MemberControllerTest {
@InjectMocks
private MemberController target;
@Mock
private MemberService memberService;
private MockMvc mockMvc;
@BeforeEach
public void init() {
mockMvc = MockMvcBuilders.standaloneSetup(target)
.setControllerAdvice(new GlobalExceptionHandler())
.build();
}
}
[ 직접 커스텀 라이브러리 구현하기 ]
결국 위의 문제점들을 정리해보면 우리에게 필요한 요구사항은 다음과 같다.
- @WebMvcTest를 사용할 것
- @MockBean과 @SpyBean을 사용하지 않고 가짜 객체를 지원할 것
- 애플리케이션 컨텍스트를 재사용할 것
1. @WebMvcTest를 사용하여 설정을 자동화할 것
@WebMvcTest를 사용했을 때의 장점 중 하나가 웹 계층 설정 자동화이다. 웹 계층 설정이 자동화 되어야만 웹 계층에 대한 설정이 변경되어도 추가적인 비용 없이 테스트 코드를 유지할 수 있다.
2. @MockBean과 @SpyBean을 사용하지 않고 가짜 객체를 지원할 것
@MockBean과 @SpyBean은 애플리케이션 컨텍스트를 새롭게 만들도록 하는 요인 중 하나이다. 그러므로 @MockBean과 @SpyBean을 사용하지 않고도 가짜 객체를 사용할 수 있어야 한다.
3. 애플리케이션 컨텍스트를 재사용할 것
@WebMvcTest가 느려지는 이유는 결국 애플리케이션 컨텍스트가 계속 새롭게 만들어지기 때문이다. 그러므로 모든 컨트롤러 테스트마다 애플리케이션 컨텍스트를 재사용해야만 테스트 속도를 증가시킬 수 있다.
하지만 위와 같은 요구 사항을 모두 들어주는 라이브러리는 없다. 그래서 이를 모두 만족시켜주는 커스텀 라이브러리를 직접 구현하였는데, 이어지는 포스팅에서 살펴보도록 하자.