API가 제대로 동작하는 것을 확인하기 위해서 통합 테스트를 작성하게 되었다. Spring Boot에서는 RestAssured라는 라이브러리를 사용해서 통합 테스트를 작성한다는 것을 알게 되었고, 하는 김에 사용 방법을 정리해보기로 했다. 참고로 이 테스트는 Testcontainers로 테스트 환경을 구축하였다.
E2E(End to End) 테스트는 실제 사용자 관점에서 애플리케이션의 전체 흐름을 검증하는 테스트 방식이다. HTTP 요청을 보내고 응답을 받기까지의 전체 플로우를 테스트하기 때문에 실제 운영 환경과 가장 유사한 조건에서 테스트할 수 있다는 장점이 있다.
E2E 테스트의 가장 큰 특징은 실제 데이터베이스를 사용한다는 점이다. 이를 통해 단순히 기능이 동작하는지를 넘어서 실제 사용자가 겪을 수 있는 시나리오를 그대로 재현할 수 있다. 또한 각 테스트는 독립적으로 실행되어 테스트 간 간섭을 방지한다.
단위 테스트는 개별 컴포넌트인 Repository나 Service만을 대상으로 테스트한다.
Mock 객체를 활용하여 외부 의존성을 제거하기 때문에 빠른 실행 속도를 보장한다.
E2E 테스트는 HTTP 요청부터 응답까지 전체 플로우를 검증한다. 실제 데이터베이스를 사용하고 운영 환경과 유사한 조건을 제공하기 때문에 통합적인 동작을 확인할 수 있다. 단위 테스트로는 발견하기 어려운 컴포넌트 간 상호작용 문제를 찾아낼 수 있다.
RestAssured는 REST API를 테스트하기 위한 Java DSL이다. HTTP 요청과 응답을 검증하는 데 특화되어 있으며, Given-When-Then 패턴을 사용하여 가독성 높은 테스트 코드를 작성할 수 있다. 또한 메서드 체이닝 방식을 통해 직관적이고 읽기 쉬운 테스트 코드를 작성할 수 있다.
RestAssured는 Given-When-Then 패턴을 기반으로 동작한다. 이 패턴은 테스트를 세 단계로 명확하게 구분하여 가독성을 높여준다.
@Test
void exampleTest() {
// given - 테스트 준비
LoginRequest request = new LoginRequest("test@example.com", "password");
// when - 실제 동작 수행
ExtractableResponse<Response> response = RestAssured.given()
.body(request)
.post("/app/auths")
.then()
.extract();
// then - 결과 검증
assertThat(response.statusCode()).isEqualTo(200);
}
Given 단계에서는 테스트에 필요한 데이터와 상태를 준비한다. 요청 객체나 헤더, 파라미터 등을 설정하는 단계다. When 단계에서는 실제 API를 호출한다. RestAssured의 메서드 체이닝을 사용하여 요청을 보낸다. Then 단계에서는 응답 상태 코드와 응답 본문 등을 검증한다.
// GET
RestAssured.given().log().all()
.when()
.get("/api/users")
.then().log().all()
.extract();
// POST (JSON)
RestAssured.given().log().all()
.body(request)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.post("/api/users")
.then().log().all()
.extract();
// PATCH
RestAssured.given().log().all()
.body(request)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.patch("/api/users/1")
.then().log().all()
.extract();
// DELETE
RestAssured.given().log().all()
.when()
.delete("/api/users/1")
.then().log().all()
.extract();
POST나 PATCH 요청처럼 Body가 필요한 경우 body() 메서드로 요청 객체를 전달하고, contentType()으로 Content-Type을 지정해주면 된다.
// 헤더
RestAssured.given()
.header("Authorization", "Bearer " + accessToken)
.when()
.get("/api/users/profile")
// 쿠키
RestAssured.given()
.cookie("refreshToken", refreshToken)
.when()
.post("/api/refresh")
// 쿼리 파라미터
RestAssured.given()
.queryParam("page", 1)
.queryParam("size", 10)
.when()
.get("/api/users")
// Path Variable
RestAssured.given()
.pathParam("userId", 123)
.when()
.get("/api/users/{userId}")
API 테스트를 작성하다 보면 인증 토큰을 헤더에 담거나 쿼리 파라미터를 전달해야 하는 경우가 많다. 특히 인증이 필요한 API를 테스트할 때는 로그인 후 받은 토큰을 헤더에 담아서 요청을 보내는 방식을 사용하면 된다.
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
| 메서드 | 용도 |
|---|---|
getString() | 이름, 이메일, 메시지 등 |
getInt() | 나이, 개수, 페이지 번호 등 |
getLong() | ID, 타임스탬프 등 |
getBoolean() | 상태, 플래그 등 |
getList() | 목록 데이터 |
| 메서드 | 설명 |
|---|---|
isEqualTo() | 정확히 일치하는지 |
isNotNull() | null이 아닌지 |
contains() | 부분 문자열 포함 |
hasSize() | 배열/리스트 크기 |
isGreaterThan() | 숫자 비교 |
// 토큰 획득
ExtractableResponse<Response> loginResponse = RestAssured.given()
.body(loginRequest)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.post("/api/auth/login")
.then()
.extract();
String accessToken = loginResponse.jsonPath().getString("result.accessToken");
// 토큰 사용
RestAssured.given()
.header("Authorization", "Bearer " + accessToken)
.when()
.get("/api/users/profile")
이런 방식으로 실제 사용자가 로그인하고 인증된 상태로 API를 호출하는 플로우를 그대로 재현할 수 있다.
RestAssured는 요청과 응답을 로깅할 수 있는 다양한 옵션을 제공한다.
given().log().all() : 모든 요청 정보given().log().headers() : 요청 헤더given().log().body() : 요청 바디then().log().all() : 모든 응답 정보then().log().status() : 응답 상태 코드then().log().ifError() : 에러 시에만 로깅testImplementation 'io.rest-assured:rest-assured'
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
@ActiveProfiles("test")
@Sql(scripts = "/sql/data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
class UserAcceptanceTest {
@LocalServerPort
int port;
@BeforeEach
void setUp() {
RestAssured.port = port;
}
@Test
void 테스트_메서드() {
// 테스트 코드 작성
}
}
모든 E2E 테스트에서 공통으로 사용할 기본 설정을 담은 클래스를 작성한다.
RANDOM_PORT : 충돌 방지를 위해 랜덤 포트를 사용한다.@DirtiesContext : 데이터 격리를 위해 각 테스트마다 Spring Context를 초기화한다.@ActiveProfiles("test") : test 프로필을 활성화한다.@Sql : 각 테스트 전에 초기 데이터를 삽입하는 SQL문을 실행한다.로그인 후 받은 토큰으로 사용자 정보를 조회하는 시나리오이다.
@DisplayName("로그인 후 정보 조회를 진행한다")
@Test
void fullUserJourney() {
// 1. 로그인
LoginRequest loginRequest = new LoginRequest(
"user@example.com", "password"
);
ExtractableResponse<Response> loginResponse = RestAssured.given().log().all()
.body(loginRequest)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.post("/app/auths")
.then().log().all()
.extract();
String accessToken = loginResponse.jsonPath().getString("result.accessToken");
Long userId = loginResponse.jsonPath().getLong("result.userId");
// 2. 사용자 정보 조회
ExtractableResponse<Response> userInfoResponse = RestAssured.given().log().all()
.header("Authorization", "Bearer " + accessToken)
.when()
.get("/app/users/" + userId)
.then().log().all()
.extract();
assertThat(userInfoResponse.statusCode())
.isEqualTo(HttpStatus.OK.value());
assertThat(userInfoResponse.jsonPath().getString("result.userInfo.email"))
.isEqualTo("user@example.com");
}
각 단계의 응답에서 필요한 데이터를 추출하여 다음 단계에 활용한다. 하드코딩된 값이 아닌 동적으로 생성된 값을 사용하기 때문에 실제 운영 환경과 동일한 플로우를 테스트할 수 있다. 또한 log().all()을 사용하여 모든 요청과 응답을 로깅하면 문제가 발생했을 때 디버깅이 훨씬 수월하다.
정상 케이스만큼 중요한 것이 예외 상황에 대한 테스트다. 잘못된 입력에 대해 적절한 에러 응답을 반환하는지 확인해야 한다.
@DisplayName("빈 이메일과 비밀번호로 로그인하면 400 Bad Request를 반환한다")
@Test
void loginWithEmptyCredentials() {
UserLoginRequest loginRequest = new UserLoginRequest("", "");
ExtractableResponse<Response> response = RestAssured.given().log().all()
.body(loginRequest)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.post("/app/auths")
.then().log().all()
.extract();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
이처럼 유효성 검증이 제대로 동작하는지, 적절한 상태 코드를 반환하는지 확인하는 테스트도 작성해야 한다.
E2E 테스트를 도입하면서 API의 전체 플로우를 검증할 수 있게 되었다. 테스트 코드 덕분에 내가 작성한 API가 의도한대로 잘 동작하는지 확인할 수 있었다. 또한 RestAssured의 문법이 직관적이라 어렵지 않게 작성할 수 있던 것 같다.
REST Assured
REST-Assured 알아보기 (테스트를 위한 클라이언트 객체)
RestAssured를 이용한 테스트 코드 작성하기
REST-assured 알아보기