본문 바로가기

백엔드

REST Assured 로 API 테스트 코드 작성하기

https://aqua-cloud.io/api-testing-guide/

도입 배경

최근 사내 코드의 일부 핵심 API를 재설계하는 과정에서, 중요한 비즈니스 로직 변경이 필요한 상황이 발생했습니다.

 

특히 Facade 레이어와 UseCase 전반에 걸쳐 대규모 수정이 예정되어 있었기 때문에,

단순한 메서드 단위나 도메인 규칙 수준의 테스트만으로는 안정성을 충분히 보장하기 어려웠습니다.

 

이에 따라, 실제 API 관점에서 발생할 수 있는 브레이킹 체인지(Breaking Change) 로 인한 장애를 사전에 방지할 필요가 있었습니다.

 

Breaking Change란 API의 스펙이 변경되면서,
기존 API를 사용하던 컨슈머들이 더 이상 정상적으로 API를 사용할 수 없게 되는 상황을 의미합니다.

이러한 문제를 방지하기 위한 대표적인 방법은 API 재설계 시
API 하위 호환성(Backwards Compatibility) 을 유지하는 것입니다.

 

 

하위 호환성을 보장하는 방법에는 여러 가지가 있지만,

이번 경우에는 “기존 API 응답이 정상적으로 유지되는가” 를 명확하게 검증하는 것이 중요하다고 판단했습니다.

 

따라서 API 레벨에서의 테스트 코드 작성을 통해 안정성을 확보하는 방향으로 접근했습니다.

 


고려했던 선택지

Option 1. CDC 기반 도구

처음에는 CDC(Consumer Driven Contract) 기반의 도구들을 고려했습니다.

CDC는 API를 사용하는 컨슈머와 이를 제공하는 프로바이더 간의 계약(Contract)을 기반으로,

API 스펙 변경에 대한 안정적인 커뮤니케이션을 보장하기 위해 발전한 개념입니다.

대표적인 도구로는 Spring Cloud Contract, Pact 등이 있습니다.

 

다만 이러한 도구들은 진입 장벽이 비교적 높고, 주로 MSA 환경에서 E2E 테스트를 보장하기 위한 목적에 더 적합하다고 판단했습니다.

이번 상황에서는 기존 스펙을 빠르게 고정하고 개선 작업에 들어가는 것이 중요했기 때문에,

보다 가볍고 즉시 적용 가능한 방법이 필요했습니다.

 

Option 2. REST Assured

REST Assured는 HTTP API를 실제로 호출하여, 응답 상태 코드, 바디, 헤더 등을 검증할 수 있는 테스트 라이브러리입니다.

자바 환경에서 간결한 문법으로 REST API 테스트를 작성할 수 있어, 빠르게 API 수준의 검증 코드를 도입하는 데 적합했습니다.

그래서 이번 요구사항에는 REST Assured를 활용하는 방식이 가장 현실적인 선택이라고 판단했습니다.


REST Assured 사용을 위한 사전 준비

먼저 build.gradle 에 다음 의존성을 추가해야 합니다.

testImplementation 'io.rest-assured:rest-assured:6.0.0'

 

Kotlin을 사용하고 있다면 다음 의존성도 함께 추가하여 유용한 확장 함수를 제공받을 수 있습니다.

testImplementation 'io.rest-assured:kotlin-extensions:6.0.0'

 

또한 Spring Framework 와의 통합을 위해 다음 의존성도 추가합니다. 

이 의존성을 추가해야 하는 이유는 글 마지막에 다루겠습니다.

testImplementation 'io.rest-assured:spring-mock-mvc:6.0.0'

REST Assured 기본 문법

REST Assured 의 기본 문법은 BDD 스타일이라고도 불리는 Given / When / Then 패턴 입니다.

  • Given : 요청의 사전 조건을 설정한다 (파라미터, 헤더, 바디 등)
  • When : 실제 HTTP 요청을 수행한다 (GET, POST, PATCH, DELETE 등)
  • Then : 응답을 검증한다 (상태 코드, 바디 필드 값 등)

사전 조건이 있는 경우 - given() 으로 시작합니다.

RestAssuredMockMvc.given()
          .contentType(ContentType.JSON)          // Given: Content-Type 헤더 설정
          .queryParam("memberId", "testMemberId") // Given: 쿼리 파라미터 설정
          .body(request)                          // Given: 요청 바디 설정
          .when()
          .post("/api/v1/notifications")          // When: POST 요청
          .then()
          .statusCode(200)                        // Then: 상태 코드 검증
          .body("success", equalTo(true))         // Then: 응답 바디 검증
          .body("data", equalTo(true));

 

사전 조건이 없는 경우 - given() 을 생략할 수 있습니다.

RestAssuredMockMvc.when()
          .get("/api/v1/posts")                     // When: 바로 GET 요청
          .then()
          .statusCode(200)
          .body("success", equalTo(true))
          .body("data", hasSize(2))
          .body("data[0].title", equalTo("제목1"));

 


다양한 요청 테스트하기

쿼리 파라미터 (Query Parameter)

쿼리 파라미터란 URL 뒤에 ?key=value 형태로 붙는 파라미터를 말합니다. 

REST Assured 에서는 queryParam() 으로 다음과 같이 설정할 수 있습니다.

RestAssuredMockMvc.given()
	.queryParam("id", "user1")
    	.queryParam("memberRole", "ADMIN")
	.queryParam("memberStatus", "ACTIVE")
	.when()
	.patch("/api/v1/members");

 

 

MULTIPART  파일 업로드 파라미터

param() 은 HTTP 메서드에 따라 Query Parameter 또는 Form Parameter로 자동 매핑 됩니다.

멀티파트 파라미터와 함께 요청하는 예제는 다음과 같습니다.

RestAssuredMockMvc.given()
          .multiPart("file", "photo.png", "image-data".getBytes(), "image/png")
          .param("category", "IMAGE")
          .param("prefixId", 1L)
          .when()
          .post("/api/v2/files/test");

 

 

JSON 요청 바디

body() 에 Java 클래스 객체를 넘기면 REST Assured가 자동으로 JSON 직렬화를 수행합니다.

이때 contentType(ContentType.JSON)을 함께 설정해야 합니다.

RestAssuredMockMvc.given()
          .contentType(ContentType.JSON)
          .body(loginRequest)     // Java 객체 → JSON 자동 변환
          .when()
          .post("/api/v1/login");

 


API 호출하고 응답 데이터 검증하기

REST Assured는 JSON 응답을 탐색할 때 GPath(Groovy의 Path 표현식) 문법을 사용합니다.

JavaScript의 점(.) 표기법과 유사하다고 볼 수 있습니다.

// 단순 필드 검증
.body("success", equalTo(true))
.body("data", equalTo(1))

// 중첩 객체 필드 접근
.body("data.id", equalTo("user1"))
.body("data.title", equalTo("공지사항"))
.body("data.content", equalTo("내용입니다"))

// 배열 크기 검증
.body("data", hasSize(2))
.body("data.boardImageList", hasSize(2))

// 배열 내 특정 인덱스 접근
.body("data[0].title", equalTo("제목1"))

 

 

왜 Hamcrest 인가?

그리고 body() 에서 값을 검증할 때는 내부적으로 Hamcrest 문법을 사용하게 되는데요

이는 REST Assured 의 검증 아키텍처 자체가 Hamcrest 문법을 사용한 JUnit 4 시절부터 쓰였기 때문입니다.

 

AssertJ나 kotlin 기반의 DSL을 써왔다면 어색할 수 있지만 기본적으로 뛰어난 가독성을 지니고 있기 때문에 유용합니다.

주요한 Hamcrest Matcher를 정리해보면 다음과 같습니다

 

  • equalTo(value) : 값이 정확히 일치하는지
  • hasSize(n) : 컬렉션이나 배열의 크기
  • hasItems(v1, v2) : 컬렉션에 특정 요소가 포함되는지
  • is(value) : equalTo 의 축약형
  • notNullValue() : null 이 아닌지
  • containsString("text") : 문자열 포함 여부
  • greaterThan(n) : 숫자 크기 비교
  • hasItem(value) : 컬렉션에 단일 요소 포함 여부

Hamcrest의 강력한 점은 Matcher 끼리 조합할 수 있다는 것입니다.

  // allOf: AND 조건
  .body("data.name", allOf(startsWith("김"), hasLength(3)))

  // anyOf: OR 조건
  .body("data.status", anyOf(equalTo("ACTIVE"), equalTo("PENDING")))

  // not: 부정
  .body("data.deletedAt", not(nullValue()))

 

이런 조합은 단순한 assertEquals 로는 표현하기 어렵죠.

 

Hamcrest Matcher는 describeTo() 와 describeMismatch() 를 통해 실패 시 

무엇을 기대했고 실제로 무엇이 왔는지를 자동으로 설명합니다. 

Expected: a collection with size <2>
       but: collection size was <3>

 

assertEquals의 단순한 expected: 2 but was: 3 보다 맥락이 풍부 합니다.


일반 RestAssured vs RestAssuredMockMvc

제가 예제 코드에서 RestAssured가 아닌 RestAssuredMockMvc 를 사용한 걸 눈치 채셨나요?

두 가지 동작 방식에 아주 큰 차이가 있습니다.

 

  RestAssured RestAssuredMockMvc
동작 방식 실제 HTTP 요청을 전송 Spring MockMvc를 통해 내부 디스패치
서버 필요 실제 서버가 떠 있어야 함 서버 없이 테스트 가능
속도 느림 (네트워크 I/O) 빠름 (인메모리)
적합한 상황 E2E 테스트, 외부 API 테스트 컨트롤러 슬라이스 테스트

 

 

RestAssuredMockMvc.mockMvc(mockMvc) 로 Spring의 MockMvc 인스턴스를 REST Assured에 주입하면, 

이후 RestAssuredMockMvc.given()...으로 작성하는 모든 테스트가 MockMvc를 통해 실행 됩니다.

 

그럼 왜 MockMvc를 직접 쓰지 않고 REST Assured로 감싸는 걸까요?

MockMvc를 직접 쓰면 이런 코드가 됩니다.

  // 순수 MockMvc — 장황하고 가독성이 떨어진다
  mockMvc.perform(
      get("/api/v1/posts")
          .queryParam("id", "1")
          .contentType(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.success").value(true))
      .andExpect(jsonPath("$.data.id").value(1))
      .andExpect(jsonPath("$.data.title").value("공지사항"));

 

반면 REST Assured MockMvc 로는 이렇게 작성할 수 있습니다.

  // REST Assured MockMvc — BDD 구조로 깔끔하다
  RestAssuredMockMvc.given()
      .queryParam("id", 1L)
      .when()
      .get("/api/v1/posts")
      .then()
      .statusCode(200)
      .body("success", equalTo(true))
      .body("data.id", equalTo(1))
      .body("data.title", equalTo("공지사항"));

 

즉, REST Assured의 DSL이 주는 가독성 이점을 유지하면서,

실제 서버를 띄우지 않는 MockMvc의 속도 이점을 동시에 누릴 수 있는 것입니다.


참고자료