[Spring] 웹 계층 테스트 방법

최재혁·2022년 11월 10일
0
post-thumbnail
post-custom-banner

🤨들어가며

전에는 @DataJpaTest 어노테이션을 사용한 Repository 계층의 테스트를 작성하는 내용에 관해서 포스팅을 올렸습니다. 오늘은 웹 계층 테스트에 대해서 알아보겠습니다.


😎통합 테스트

통합 테스트 시에 사용하는 @SpringBootTest 어노테이션에 여러가지 webEnvironment 옵션들을 주어, 웹 환경을 설정할 수 있습니다.

1. @SpringBootTest(webEnvironment = WebEnvironment.Mock)

기본값으로 설정되어 있는 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에서 받아온 모든 요청, 응답 정보
    }
}

2. @SpringBootTest(webEnvironment = WebEnvironment.[RANDOM_PORT], [DEFINED_PORT])

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 사용 용도에 관해서...]

그렇다면, 언제 내장 톰캣을 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

@WebMvcTest는 웹 계층 슬라이스 테스트를 위한 어노테이션입니다. 통합 테스트의 WebEnvironment=RANDOM_PORT 옵션과 같이, 랜덤으로 포트를 열어 내장 톰캣을 띄웁니다. 하지만 @WebMvcTest 어노테이션은, 웹 계층과 관련된 항목들만 빈으로 등록합니다.

  • @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, WebMvcConfigurer , HandlerMethodArgumentResolver 항목들만 스프링 컨텍스트에 빈으로 등록

Service, Repository와 같은 다른 계층의 항목들은 빈으로 등록이 되지 않기 때문에, @SpringBootTest를 사용하는 것보다 더 가볍습니다. 하지만 그 말은, Controller 계층에서 하위 계층에 의존성을 주입받고 있다면 그 연결도 끊긴다는 의미의므로, @MockBean 어노테이션을 사용하여 주입받아야 합니다. 안 그러면 UnsatisfiedDependencyException이 발생해요.

💥그런데 어디까지 @MockBean ??

@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 호출 로직 선택에 포함될 내용이었습니다. 하지만 내용이 너무 길어지는 것 같아서 중간에 잘랐습니다. 그래서 마무리가 좀 뭔가 어색해도 이해해주시면 감사하겠습니다.

📚Reference

https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/context/SpringBootTest.html

https://ict-nroo.tistory.com/96

https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.html

https://www.baeldung.com/spring-5-webclient![](https://velog.velcdn.com/images/chlwogur2/post/96640c18-f386-4084-9dfb-0665c0ca533b/image.md)

profile
잘못된 고민은 없습니다
post-custom-banner

0개의 댓글