애플리케이션을 개발할 때 기능이 정상적으로 동작하는지 확인하는 것은 매우 중요하다. 단순히 코드 단위에서 검증하는 것을 넘어, 실제 사용자의 입장에서 요구사항이 잘 반영되었는지 인수 테스트(Acceptance Test)를 통해 확인할 수 있다.
인수 테스트란?
인수 테스트는 소프트웨어 개발에서 기능이 구현된 후, 사용자가 기대한 대로 동작하는지를 검증하는 과정이다. 인수 테스트에서 인수(Acceptance)는 승인이라는 의미로, 클라이언트가 기능을 검토하고 승인하는 단계에서 수행되는 테스트이다. 따라서 인수 테스트가 통과되면, 해당 기능이 요구사항을 만족하므로 작업을 종료할 수 있다.
인수 테스트는 단위 테스트나 통합 테스트보다 더 넓은 범위를 검증하는데, 그만큼 실행 비용이 높은 편이다. 그리고 Spring에서 인수 테스트 도구로 오픈 소스인 RestAssured를 많이 사용한다.
MockMvc vs RestAssured
Spring 환경에서 API 테스트를 진행할 때, 대표적으로 MockMvc와 RestAssured 두 가지 방법이 있다. 두 도구는 비슷하면서도 다른 점이 있다.
MockMvc는 컨트롤러 레이어의 테스트를 수행하는 데 적합한 도구이다. 실제 서블릿 컨테이너를 실행하지 않고, mocking된 웹 환경에서 컨트롤러 로직을 검증할 수 있도록 돕는다.
- @SpringBootTest가 아닌 @WebMvcTest를 사용하여 컨트롤러와 관련된 빈만 로드
- 서비스나 리포지토리 빈은 Mock 객체로 대체하여 순수하게 컨트롤러 로직만 검증
- 테스트 실행 속도가 빠름
반대로 RestAssured는 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 또는 WebEnvironment.DEFINED_PORT 설정을 사용해 내장 서버(apache tomcat)를 실행한 후, 실제 API 요청을 보내고 응답을 검증하는 도구이다.
- 실제 웹 환경에서 HTTP 요청을 테스트
- Mocking 없이 애플리케이션 전반을 검증할 수 있음
- 테스트 속도가 상대적으로 느림
결론적으로, MockMvc는 컨트롤러 레이어의 빠른 단위 테스트에 적합하며, RestAssured는 실제 서버 환경에서 API를 호출해 전체적인 동작을 검증하는 데 유용하다.
인수 테스트 작성
스프링부트 애플리케이션에서 RestAssured를 사용해 인수 테스트를 작성하는 방법을 알아보자. 참고로 인수 테스트는 블랙 박스 테스트의 성격을 가지며, 내부 구현보다 결과에 초점을 맞춘다. 따라서 요청을 통해 기능을 검증하고, 직접 요청을 통해서 초기화 데이터를 만들어야 한다.
실습 환경
- Spring Boot 3.4.1
- rest-assured 5.5.0
인수 테스트를 작성하기에 앞서, 인수 테스트에서 공통으로 사용하는 설정, 즉 환경을 추상 클래스로 분리하여 관리를 용이하게 하였다.
@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class AcceptanceTest {
@LocalServerPort
protected int port;
protected static RequestSpecification REQUEST_SPEC;
@BeforeEach
public void setUp() {
REQUEST_SPEC = new RequestSpecBuilder()
.setPort(port)
.log(LogDetail.ALL)
.addHeader("Content-Type", "application/json")
.build();
}
}
간단하게 카페 키오스크에서 메뉴를 조회하는 요구 사항에 대한 인수 테스트를 작성해보자. given 절에서 메뉴를 생성하고 when 절에서 /api/menu 라는 uri로 GET 요청을 보낸 뒤 받은 응답을 then 절에서 검증하고 있다.
@DisplayName("메뉴를 조회한다.")
@Test
void getMenu() {
// given
Menu 아메리카노 = Menu.builder()
.menuType(MenuType.DRINK)
.name("아메리카노")
.price(BigDecimal.valueOf(2000L))
.description("투 샷")
.build();
Menu 카페_라떼 = Menu.builder()
.menuType(MenuType.DRINK)
.name("카페 라떼")
.price(BigDecimal.valueOf(2500L))
.description("투 샷 + 우유")
.build();
Menu 케이크 = Menu.builder()
.menuType(MenuType.DESSERT)
.name("케이크")
.price(BigDecimal.valueOf(5000L))
.description("딸기 생크림")
.build();
menuRepository.saveAll(List.of(아메리카노, 카페_라떼, 케이크));
// when
ExtractableResponse<Response> response = given()
.spec(REQUEST_SPEC)
.param("size", 10, "page", 0)
.when()
.get("/api/menu")
.then()
.log().all()
.statusCode(200)
.extract();
// then
response.as(ApiResponse.class);
List<MenuResponses.MenuResponse> responses = response.body()
.jsonPath()
.getList("data.menus", MenuResponses.MenuResponse.class);
assertThat(responses).hasSize(3);
}
하지만 이 테스트 코드는 문제가 있다. given 절에서 데이터를 builder로 생성하는 것은 비즈니스 정합성에 어긋하는 데이터가 생성될 수 있어 테스트가 깨질 위험이 있다. 따라서 블랙 박스 테스트라는 인수 테스트의 성격에 맞게 API 요청을 통해 데이터를 생성해야 한다.
@DisplayName("메뉴를 조회한다.")
@Test
void getMenuV2() {
Long menu1 = createMenu();
Long menu2 = createMenu();
Long menu3 = createMenu();
// when
ExtractableResponse<Response> response = given()
.spec(REQUEST_SPEC)
.param("size", 10, "page", 0)
.when()
.get("/api/menu")
.then()
.log().all()
.statusCode(200)
.extract();
// then
response.as(ApiResponse.class);
List<MenuResponses.MenuResponse> menus = response.body()
.jsonPath()
.getList("data.menus", MenuResponses.MenuResponse.class);
assertThat(menus).hasSize(3);
}
private Long createMenu() {
MenuCreateRequest request = MenuCreateRequest.builder()
.type("DRINK")
.name("아메리카노")
.price(BigDecimal.valueOf(2000L))
.description("투 샷")
.build();
ExtractableResponse<Response> response = given()
.spec(REQUEST_SPEC)
.body(request)
.when()
.post("/api/menu")
.then()
.log().all()
.statusCode(200)
.extract();
return response.body()
.jsonPath()
.getLong("data.menuId");
}
통합 테스트와 마찬가지로 외부 모듈에 의존적인 로직이 있는 API는 인수 테스트하기 어렵다. 테스트 계정을 통해 실제 외부 서버에 요청하거나 테스트 환경에서는 Fake 서비스를 호출하도록 하는 방향으로 해결할 수 있다.
다음으로 RestAssured는 BDD 스타일로 작성되며, 반복적으로 작성해줘야 하는 코드가 존재한다. 따라서 IntelliJ의 Live Templates를 활용하면 생산성을 향상시킬 수 있다. 나는 다음과 같이 템플릿을 만들어두었다.
@DisplayName("")
@Test
void $METHOD_NAME$() {
// given
// when
ExtractableResponse<Response> response = given()
.spec(REQUEST_SPEC)
.when()
.$HTTP_METHOD$($URI$)
.then()
.log().all()
.statusCode($STATUS$)
.extract();
// then
response.as(ApiResponse.class);
}
'Server' 카테고리의 다른 글
[Architecture] 사용자 수에 따라 시스템 설계하기 (0) | 2024.03.09 |
---|