스프링 부트로 웹 애플리케이션을 만들다 보니, controller 또한 테스트의 영역이 되었습니다.
아래 코드는 예약을 생성하는 api입니다.
controller 로직을 어떻게 테스트할 수 있을까요?
웹 애플리케이션에 대해 알지 못했다면
아래 코드처럼, 직접 createReservationByLoginMember 메서드를 호출하여 단위테스트로 구현했을 것 같습니다.
그러나, 이런 방식으로 controller를 테스트하진 않겠죠.
왜일까요?
웹 애플리케이션에서 controller의 동작의 핵심은 HTTP 요청을 받아, 관련 모듈에게 협력을 요청하고 응답을 생성하여 다시 반환해 주는 것입니다. 즉, 웹 애플리케이션의 요청과 응답 사이의 상호 작용을 담당합니다.
이 부분까지 테스트를 하려면 어떻게 하면 좋을까요?
RestAssured
개념
REST Assured is a Java DSL for simplifying testing of REST based services built on top of HTTP Builder. It supports POST, GET, PUT, DELETE, OPTIONS, PATCH and HEAD requests and can be used to validate and verify the response of these requests.
공식 문서에 의하면, REST 기반 서비스를 테스트하는 라이브러리 중 하나입니다.
DSL(Domain Specific Language)이란, 비즈니스 도메인의 문제를 해결하기 위해 만든 작고 범용이 아닌 특정 도메인을 대상으로 한 특수 프로그래밍 언어를 뜻합니다.
데이터베이스 쿼리를 작성하기 위한 DSL인 SQL, 정규 표현식을 위한 DSL인 Regex과 같은 개념입니다.
스프링 프레임워크에서 제공하는 라이브러리가 아니므로 의존성을 별도로 추가해줘야 합니다.
검증
RestAssured로 예약 생성하는 로직을 테스트해 보겠습니다.
@SpringBootTest 애너테이션을 붙여서 스프링 컨테이너에 모든 Bean을 등록합니다.
로그인한 회원이 예약을 생성하는 E2E 테스트를 작성했습니다.
위 테스트 코드에서 검증하는 부분은 다음과 같습니다.
- /login으로 POST 요청을 보내 응답 코드 200을 받는다.
- /reservations으로 POST 요청을 보내 응답 코드 201을 받고, 응답 데이터를 검증한다.
특징
1. 실제 환경과 유사한 테스트를 할 수 있다.
RestAssured는 실제 웹 서버(Tomcat)를 구동시켜 로직을 테스트할 수 있습니다. 실제 HTTP 요청을 보내고 응답을 받습니다. 덕분에, 외부 서버로도 요청을 보낼 수 있습니다.
2. BDD 스타일(given-when-then)로 작성할 수 있다.
BDD는 TDD(Test Driven Development)에 DDD(Domain Driven Design)의 스타일을 적용한 개발의 한 방식입니다. 특히 설계적인 측면을 강조합니다.
3. 요청 데이터를 지정(specifying)할 수 있다.
공식 Wiki에 문법이 상세히 나와 있습니다.
parameters, headers, cookies, body, content type에 원하는 값을 넣어 요청할 수 있습니다.
4. 응답 데이터 검증(verifying)과 추출(extracting) 할 수 있다.
status code, status line, cookies, headers, content type, body를 검증할 수 있는 문법을 지원합니다.
응답 시간을 측정하는 문법도 있습니다.
when().
get("/lotto").
then().
time(lessThan(2L), SECONDS);
extract() 메서드로 데이터를 추출하여 AssertJ나 JUnit으로 검증할 수도 있습니다.
유의점
RestAssured를 사용할 때 유의해야 할 점이 있습니다.
1. Default 설정 localhost:8080
기본적으로 localhost:8080을 가정하고 요청을 수행합니다.
따라서, WebEnvironment를 RANDOM_PORT로 지정한 경우 RestAssure의 port를 맞춰줘야 합니다.
2. @Transactional 정상 동작하지 않는 문제
같은 스레드가 아닌 경우, @Transactional이 의도한 대로 동작하지 않습니다.
예시를 통해 확인해 보겠습니다.
memberService의 create 메서드를 직접 호출하여 데이터를 저장하고
RestAssured로 회원 목록을 조회하는 요청을 보내는 코드입니다.
그러나, 데이터 3건을 의도했지만 2건만 조회됩니다. create 메서드를 호출하여 저장한 데이터가 반영되지 않았습니다.
생성과 조회 메서드가 호출될 때 스레드를 확인해 보니, 다른 스레드에서 동작한 것을 알 수 있었습니다.
반대로, RestAssured로 생성과 조회 요청을 보내보겠습니다.
의도한 3개의 데이터가 조회되었고, 같은 스레드에서 동작하는 것을 확인할 수 있었습니다.
지금까지 RestAssured로 controller를 검증해 보았습니다.
그러나, 모든 레이어를 검증하지 않고 controller 레이어만 테스트해 볼 수 없을까요?
MockMvc
개념
The Spring MVC Test framework, also known as MockMvc, provides support for testing Spring MVC applications.
It performs full Spring MVC request handling but via mock request and response objects instead of a running server.
공식 문서에 의하면, MockMvc는 서버를 실행하지 않고 모의 요청과 응답 객체를 통해 Spring MVC 테스트 환경을 구축해 줍니다.
Spring 공식 스펙이라는 점도, RestAssured과 차이가 있습니다.
검증
MockMvc로 예약 생성하는 로직을 테스트해 보겠습니다.
@MockMvcTest 애너테이션을 붙이고, 빈으로 등록하고자 하는 ReservationController 클래스를 value로 설정합니다. Mocking할 클래스는 @MockBean 애너테이션을 붙여줍니다.
코드 상 ReservationController 전에 ArgumentResolver와 Interceptor를 거치게 되어 있는데,
검증 대상이 아닌 부분은 빈 등록에서 제외해 줍니다. 대신, WebMvcConfiguration을 대신할 TestWebMvcConfiguration 클래스를 빈으로 등록합니다.
TestWebConfiguration가 LoginMemberArgumentResolver를 Stub 한 객체를 사용하도록 구현했습니다.
다시 테스트 코드 돌아와서,
when 절에서 reservationService.create를 호출하면, 지정된 응답(savedReservation)을 반환하도록 합니다.
MockMvc 구문에서 예약을 생성하는 요청을 보냅니다.
RestAssured과 유사하게 /reservations으로 POST 요청을 보내 응답 코드 201(created)을 받고, 응답 데이터를 검증하고 있습니다.
단, controller 레이어의 로직만 실제 동작하고 있고, 이전과 이후 로직은 가짜로 동작합니다.
요청과 응답 데이터를 확인할 수 있습니다.
특징
1. 웹 서버(servlet container)를 띄울 필요가 없어 빠르다.
RestAssured 로그(위쪽)는 Tomcat 웹 서버가 띄워진 반면, MockMvc 로그(아래쪽)는 웹 서버가 띄워지지 않고 TestDispatcherServlet이 생성된 것을 확인할 수 있습니다.
테스트 로직은 다르지만, 실행 시간도 유의미한 차이가 있었습니다.
2. SliceTest 가능하다.
Mocking을 통해 하나의 레이어만 슬라이스 하여 단위 테스트를 할 수도 있고, 두 개 이상의 레이어만 통합 테스트 할 수도 있습니다.
3. @WebMvcTest 사용 시, MockMvc 인스턴스 자동 구성된다.
주로 MockMvc로 controller 테스트 시에 @WebMvcTest 애너테이션을 추가하는 경우가 많지만, 상황에 따라 @SpringBootTest 애너테이션을 붙여서 모든 빈을 등록해 주는 것이 나을 수도 있습니다.
단, @SpringBootTest 사용 시 @AutoConfigureMockMvc 추가해 주어야 MockMvc 인스턴스를 사용할 수 있습니다.
4. Perform-expect-do-return 동작 수행한다.
API 테스트를 위한 여러 문법을 제공합니다. 상세한 문법은 공식 문서에서 확인할 수 있습니다.
- 요청 생성(post, get, put, patch, delete...)
- 검증(status, header, content, jsonPath, cookie, view...)
- 동작 수행(log, print)
- 반환(MvcResult)
유의점
@SpringBootTest 애너테이션과 함께 사용 시 WebEnvironment를 RANDOM_PORT, DEFINED_PORT 조합으로 사용하지 않는 것입니다.
MockMvc는 TestDispatcherServlet을 생성하고, @SpringBootTest(RANDOM_PORT or DEFINED_PORT)가 Tomcat 서버를 띄우기 때문입니다.
이렇게 사용하면, 빠른 속도의 MockMvc 이점을 취하지 못하겠죠.
@SpringBootTest vs @WebMvcTest
계속 등장한 애너테이션이 있어 간단히 짚고 넘어가겠습니다.
두 애너테이션은 스프링의 테스트 환경을 구축해 줍니다.
@SpringBootTest
애플리케이션의 전체 context를 로드합니다. 빈이 이미 등록되어 있으니, @Autowired 애노테이션 만으로 주입받아 사용할 수 있습니다.
전체 컨텍스트가 필요하거나 통합 테스트를 하고 싶은 경우 사용될 수 있습니다.
WebEnvironment는 4가지가 있습니다. MOCK이 default입니다.
RANDOM_PORT와 DEFINED_PORT는 내장된 embedded 서버를 사용합니다.
@WebMvcTest
web 관련 context만 로드합니다.
@Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations, HandlerMethodArgumentResolver
단, @Component와 @ConfigurationProperties 이 붙은 클래스는 등록되지 않습니다.
빈으로 등록하고 싶은 클래스는 @Import 애너테이션 등으로 명시해줘야 합니다.
controller 관련된 컨텍스트만 필요하거나, controller layer만 slice test 하고 싶은 경우
가볍고 빠르게 테스트하는 경우 사용될 수 있습니다.
지금까지, 두 라이브러리를 사용하여 controller를 테스트해 보았습니다.
그러나 두 방식은 용도의 차이가 분명했습니다.
테스트를 위해 도구를 먼저 생각하기보다, 테스트할 범위 정하는 것이 선행되어야 할 것 같습니다.
controller를 어떻게 테스트하고 싶은 지를 먼저 정하고, 그 후 자신에게 편한 도구를 선택하는 것이 좋을 것 같습니다.
정리
어떻게 테스트해야 하는가?
- 테스트할 범위를 정하고, 그에 맞게 도구를 선택하자.
RestAssured
- 실제 서버에 요청을 보내 검증한다.
- 주로, @SpringBootTest 함께 사용한다.
- E2E 테스트에 특화되어 있다.
WebMvc
- 웹 서버를 띄우지 않고, 실제 요청을 보내지 않는 방식이다.
- @WebMvcTest,@SpringBootTest(WebEnvironment.MOCK)과 함께 사용된다.
- Slice Test 할 수 있다.
참고
공식 문서
- https://github.com/rest-assured/rest-assured/wiki/Usage
- https://docs.spring.io/spring-framework/reference/testing/spring-mvc-test-framework.html
- https://spring.io/guides/gs/testing-web
- https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.spring-boot-applications.spring-mvc-tests
블로그
- https://tecoble.techcourse.co.kr/post/2020-08-19-rest-assured-vs-mock-mvc/
- https://rieckpil.de/choosing-between-mockmvc-and-springboottest-for-testing/
- https://www.baeldung.com/spring-mockmvc-fetch-json
유튜브
'Spring' 카테고리의 다른 글
👣 로그 수집과 모니터링 구축기 (2) | 2024.11.20 |
---|---|
코드로 더미 데이터를 추가해보자 (21) | 2024.05.27 |
SpringBootTest 격리 방법 : @DirtiestContext의 대체재 (0) | 2024.05.06 |
@Component와 @Repository는 무엇이 다른가요? (49) | 2024.04.22 |