본문 바로가기
Java & Kotlin/Spring

여러 유저를 사용한 E2E test (RestAssured 사용)

by devson 2025. 5. 6.

플랫폼 서비스의 경우 서비스 내에서 다양한 역할이 존재하며, 도메인/서비스에 따라 구체적인 역할이 나뉘게 된다.

간단한 커머스 서비스에 예로 들자면 다음과 같은 역할들이 있다.

  • 내부적으로 CS 처리나 운영에 있어 사용되는 Admin
  • 서비스를 통해 물건을 구매하는 Buyer
  • 서비스를 통해 물건을 판매하는 Seller

 

시스템에 따라 이러한 역할에 대한 계정 시스템이 분리될 수도 있지만,

(특히 어드민은 사용자가 사용하는 서비스와 분리되기 때문에 어드민 계정 시스템은 서비스 계정 시스템과 분리되어 관리되는 경우가 있다)

시스템에 따라 여러 역할이 모두 하나의 계정 시스템 내에서 관리되기도 한다.

 

그리고 '물품 구매와 구매 물품 리뷰'에 대해 다음과 같은 시나리오를 생각할 수 있다.

  1. Seller 역할을 가진 유저가 판매할 물품을 올린다.
  2. Buyer 역할을 가진 유저가 물품을 구매한다.
  3. Buyer 역할을 가진 유저가 구매한 물품을 구매 확정을 한다.
  4. Buyer 역할을 가진 유저가 구매한 물품에 대한 리뷰를 남긴다.
  5. Seller 역할을 가진 유저가 리뷰를 확인하고 보너스 포인트/이벤트 쿠폰을 지급한다.

 

이 시나리오에 대해 RestAssured를 통해 static method를 사용한 E2E 테스트를 작성한다면 아래와 같이 작성할 수 있다.

(각 함수의 구현은 RestAssured를 통한 API 호출이다)

@Test
fun `물품 구매 리뷰 프로세스 시나리오 테스트`() {
    val sellerAccessToken = getSellerAccessToken()
    val buyerAccessToken = getBuyerAccessToken()

    val `판매 물품` = `판매 물품을 올린다`(token = sellerAccessToken, name = "티셔츠", price = 25_000, quantity = 50)
    val 주문 = `물품을 구매한다`(token = buyerAccessToken, productId = `판매 물품`.id, quantity = 2, paymentMethod = "CREDIT_CARD")
    `주문 구매 확정한다`(token = buyerAccessToken, orderId = 주문.id)
    val 리뷰 = `구매한 물품에 리뷰를 남긴다`(token = buyerAccessToken, orderId = 주문.id, productId = `판매 물품`.id)
    `리뷰를 남긴 구매자에게 보너스 포인트를 지급한다`(token = sellerAccessToken, reviewId = 리뷰.id, bonusPoint = 2_000)
}

 

보시다시피 인가 처리에 JWT를 쓴다면 요청마다 Access Token을 header에 담아 보내야하기 때문에

여러 역할에 걸쳐 테스트 조건을 마련해야하는 경우 테스트 코드를 작성하는게 조금은 복잡해지고 코드에 중복이 생길 수 있다.

(즉, 가독성과 유지보수성이 떨어진다)

 

이러한 코드를 개선해서 코드의 중복은 없애고 표현력을 높일 수 있는 방법으로는 요청에 대한 간단한 wrapper class를 사용하는 것이다.

아래와 같이 TestClient를 만들고 RestAssured의 인터페이스인 Given, When을 그대로 사용할 수 있도록 한다.

그리고 static factory 메서드를 추가한다면 각 역할 별로 유저를 생성해서 사용할 수 있다.

import io.restassured.RestAssured
import io.restassured.response.Response
import io.restassured.specification.RequestSender
import io.restassured.specification.RequestSpecification

class TestClient(
    val accessToken: String,
) {
    fun Given(block: RequestSpecification.() -> RequestSpecification): RequestSpecification =
        authenticated().given().run(block)

    fun When(block: RequestSender.() -> Response): Response =
        authenticated().`when`().run(block)

    // 매 요청별로 생성해야한다. 그렇지 않으면 header가 계속 추가된다.
    private fun authenticated() = RestAssured.given()
        .header("Authorization", "Bearer $accessToken")
    
    companion object {
        fun admin(): TestClient {
            // 회원가입 요청
            // 로그인 요청
            // 로그인 요청의 응답에서 받은 access token 사용
            return TestClient(accessToken = "admin")
        }

        fun buyer(): TestClient {
            return TestClient(accessToken = "buyer")
        }

        fun seller(): TestClient {
            return TestClient(accessToken = "seller")
        }
    }
}

 

그리고 이를 사용해서 아래와 같이 E2E 테스트 코드를 작성할 수 있을 것이다.

(각 메서드는 TestClient에 직접 메서드를 추가하거나 extension function으로 구현하면 되겠다 - 개인적으로는 extension function으로 사용하는게 도메인 별 API 분리가 편해서 추천한다)

@Test
fun `물품 구매 리뷰 프로세스 시나리오 테스트`() {
    val seller = TestClient.seller()
    val buyer = TestClient.buyer()

    val `판매 물품` = seller.`판매 물품을 올린다`(name = "티셔츠", price = 25_000, quantity = 50)
    val 주문 = buyer.`물품을 구매한다`(productId = `판매 물품`.id, quantity = 2, paymentMethod = "CREDIT_CARD")
    buyer.`주문 구매 확정한다`(orderId = 주문.id)
    val 리뷰 = buyer.`구매한 물품에 리뷰를 남긴다`(orderId = 주문.id, productId = `판매 물품`.id)
    seller.`리뷰를 남긴 구매자에게 보너스 포인트를 지급한다`(reviewId = 리뷰.id, bonusPoint = 2_000)
}

 

이렇게 간단한 client class를 사용하여 각 유저별로 API 호출 객체를 따로 관리할 수 있게되고

테스트 시나리오에 있는 actor를 보다 명확하게 구분할 수 있게된다.

댓글