전에는 @DataJpaTest
어노테이션을 사용한 Repository 계층의 테스트를 작성하는 내용에 관해서 포스팅을 올렸습니다. 오늘은 웹 계층 테스트에 대해서 알아보겠습니다.
통합 테스트 시에 사용하는 @SpringBootTest
어노테이션에 여러가지 webEnvironment 옵션들을 주어, 웹 환경을 설정할 수 있습니다.
기본값으로 설정되어 있는 WebEnvironment.Mock
옵션입니다. 웹 환경을 Mock으로 설정하여, ServletContainer를 테스트 용으로 띄우지 않고, 서블릿을 mocking
한 것이 동작합니다. 즉 SpringBoot의 Servletcontainer인 내장 톰캣이 구동되지 않습니다. (DispatcherServlet
은 생성됩니다.) Mocking된 서블릿에는 MockMvc
를 통해 접근할 수 있습니다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc // MockMvc 타입 빈 등록
class HomeControllerTest {
@Autowired
MockMvc mockMvc; // Mock으로 등록된 서블릿에 접근
@Test
void homePageMockMvc() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk()) // 바라는 status 결과
.andExpect(content().contentType("text/html;charset=UTF-8")) // 바라는 return 값
.andExpect(view().name("home/home")) // 바라는 view 파일
.andDo(print()); // mockMvc에서 받아온 모든 요청, 응답 정보
}
}
RANDOM_PORT, DEFINED_PORT 옵션 모두 실제로 ServletContainer를 호출합니다. 즉 내장된 톰캣이 동작하게 됩니다. 가용한 Random으로 생성된 포트 (RANDOM_PORT), 혹은 기본 설정한 포트 (DEFINED_PORT)를 사용합니다. 이 경우에는 MockMvc
를 사용하지 않고, TestRestTemplate
이나 WebTestClient
를 사용하여 응답을 주고받을 수 있습니다. 둘의 차이에 대해선 이후에 설명하겠습니다. 아래는 WebTestClient
를 사용한 코드입니다.
@SpringBootTest(webEnvironment = SpringBootTest.webEnvironment.RANDOM_PORT) // 실제 내장 톰캣 작동
class HomeControllerTest {
@Autowired
WebTestClient webTestClient;
@Test
void homePageWebTestClient(){
webTestClient.get().uri("/").exchange()
.expectStatus().isOk()
.expectHeader().valueEquals("Content-type", "text/html;charset=UTF-8")
.expectBody().consumeWith(System.out::println);
}
}
그렇다면, 언제 내장 톰캣을 Mocking 해야 할까요??
Controller까지 진행되는 과정의 Filter
, Interceptor
동작을 테스트하려면 톰캣을 띄워 실제 HttpRequest, HttpResponse를 사용할 수 있도록 테스트하는 것이 좋고@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
,
Controller 자체의 동작이나 이후의 비즈니스 로직에 집중하려면 테스트 경량화를 위해 톰캣을 Mocking하는 것이 좋을 것 같습니다(@SpringBootTest(webEnvironment = WebEnvironment.Mock)
.
@SpringBootTest
는 실행 시에 실제 Spring Context에 등록된 빈들을 전부 스캔하고 가져오기 때문에, 실행속도가 느리고 무겁습니다. 그런 상황에서는 Web 계층만을 위한 슬라이스 테스트를 진행할 수 있습니다. 이번에 공부하면서 알게 되었는데, 슬라이스 테스트 != 단위 테스트라고 합니다. 왜냐하면 슬라이스 테스트는 Spring Context를 구성하기 때문입니다.
실제로 웹 계층의 슬라이스 테스트를 진행하기 위한
@WebMvcTest
어노테이션은org.springframework.boot.test.autoconfigure.web.servlet
패키지 하위에 등록되어 있습니다. 즉, 스프링부트가 제공하며, 스프링 통합 테스트를 위한 어노테이션입니다.
@WebMvcTest
는 웹 계층 슬라이스 테스트를 위한 어노테이션입니다. 통합 테스트의 WebEnvironment=RANDOM_PORT
옵션과 같이, 랜덤으로 포트를 열어 내장 톰캣을 띄웁니다. 하지만 @WebMvcTest
어노테이션은, 웹 계층과 관련된 항목들만 빈으로 등록합니다.
@Controller
, @ControllerAdvice
, @JsonComponent
, Converter
, GenericConverter
, Filter
, WebMvcConfigurer
, HandlerMethodArgumentResolver
항목들만 스프링 컨텍스트에 빈으로 등록Service
, Repository
와 같은 다른 계층의 항목들은 빈으로 등록이 되지 않기 때문에, @SpringBootTest
를 사용하는 것보다 더 가볍습니다. 하지만 그 말은, Controller
계층에서 하위 계층에 의존성을 주입받고 있다면 그 연결도 끊긴다는 의미의므로, @MockBean
어노테이션을 사용하여 주입받아야 합니다. 안 그러면 UnsatisfiedDependencyException
이 발생해요.
@WebMvcTest
는 말씀드린 웹 계층의 항목들을 모두 빈으로 등록합니다. 그렇다면, 어플리케이션에 등록된 컨트롤러의 개수가 많으면 많아질 수록, 각각의 컨트롤러가 주입받는 Service 계층들을 전부 @MockBean
으로 등록해야 할까요?
물론 아닙니다.
@WebMvcTest({테스트할 컨트롤러.class})
로 등록할 웹 계층의 빈을 특정할 수 있습니다. @WebMvcTest({SimpleController.class})
와 같이 지정하면 SimpleController가 의존하는 빈들만 @MockBean
으로 주입해주면 되는 거죠!
[AccountService, SummonerService, MatchService를 주입받는 HomeController 테스트]
@WebMvcTest({HomeController.class})
class HomeControllerTest {
@Autowired
WebTestClient webTestClient;
@MockBean
AccountService accountService;
@MockBean
SummonerService summonerService;
@MockBean
MatchService matchService;
@Test
void homePageWebTestClient() {
webTestClient.get().uri("/").exchange()
.expectStatus().isOk()
.expectHeader().valueEquals("Content-type", "text/html;charset=UTF-8")
.expectBody().consumeWith(System.out::println);
}
}
사실 오늘의 내용은 바로 뒤에 이어질 포스팅인 API 호출 로직 선택에 포함될 내용이었습니다. 하지만 내용이 너무 길어지는 것 같아서 중간에 잘랐습니다. 그래서 마무리가 좀 뭔가 어색해도 이해해주시면 감사하겠습니다.