들어가며

안녕하세요. QA Engineer 미키입니다.

이 글에서는 트렌비 프론트 서비스의 더 나은 서비스 품질보증을 위해 도입한 API 테스트 자동화에 대해서 이야기 해보고자 합니다.

배경 및 목적

트렌비는 Selenium을 통한 UI 테스트 자동화를 업무에 적용시켜 회원가입, 로그인, 결제, 장바구니, 전시영역에 이르는 대부분의 주요 구매 플로우(flow) 영역을 자동화하여 테스트하고 있습니다.

하지만 UI 테스트 자동화를 서비스의 모든 영역으로 확장하여 적용하기에는 몇가지 어려움이 있었고, 이 문제를 보완하고자 다른 방안을 생각해보게 되었습니다.

저희가 파악한 UI 테스트 자동화의 단점으로는 전수 테스트를 하는데 시간이 오래 걸리는 문제UI의 잦은 변화로 인한 유지보수 리소스 문제 였습니다.

먼저, 전수 테스트시에 시간이 오래 걸리는 문제 에 대해 살펴보면 트렌비는 지속적인 배포(Continuous Delivery)를 지향하고, 이를 위해 모든 CI/CD 파이프라인은 빠른 빌드와 배포에 초점을 맞추고 있는 상황이었습니다.

따라서, 빠른 테스트 수행은 이를 위한 필수적인 요소 중 하나였습니다. 전수 테스트를 하는데 시간이 오래 걸리면 그만큼 배포도 늦어 질 수 있기 때문입니다.

또 다른 문제는 UI의 잦은 변화로 인한 유지보수 리소스 문제 였습니다. 여러 기능을 지속적으로 배포하다 보니 이에 따른 UI 변경도 자주 발생했는데요. 이러한 UI 변경 시마다 테스트 코드도 함께 수정해야 하는 어려움이 있었습니다.

변경된 UI에 대응하는 테스트 코드 수정을 배포 전에 미리 하지 않을 경우 테스트가 실패하여 오류가 발생했고 이는 불필요한 알람을 야기했습니다.

지금까지 살펴본 단점을 보완하면서 자동화된 QA 테스트 영역을 확장하기 위해서 여러가지 방안들을 고민하게 되었고 API 테스트 자동화가 하나의 대안이 될 수 있다고 판단 했습니다.

화면보다는 데이터가 중요한 부분에 API 테스트 자동화를 병행하면 전수 테스트를 하는데 시간을 절약 할 수 있고, 잦은 UI변경에도 대처 할 수 있기 때문입니다.

API 테스트 자동화의 접근

API 테스트 자동화를 도입하기로 결정하고 나니 API 테스트를 어떻게 해야할지(?) 고민하게 되었고 대략 두가지 방법을 생각하게 되었습니다.

  1. Postman과 같은 잘 만들어진 툴을 사용하여 API 테스트를 한다.
  2. Python에서 HTTP 요청을 보내는 모듈인 Requests를 사용한다.

두 가지 방안에 대해 조사하고 POC를 진행하였는데 현재 트렌비의 경우 자동화 스크립트를 Python으로 관리하고 있기에 유지보수 측면에서 두 번째 방법이 더 효율적이라 판단되어 Python Requests를 이용하기로 했습니다.

목표

API가 정상적으로 응답하는지 테스트 하려면 어떻게 해야 할까!? 이를 검증하기 위해서 3가지 목표를 설정했습니다.

  • API 상태코드를 체크하자!
  • API가 정상동작 할 때 적절한 응답 및 데이터가 오는지 체크하자!
  • 유저시나리오(로그인부터 주문까지) 기반 핵심 API가 정상동작 하는지 체크하자!

첫 번째. GET방식(requests.get())을 통한 상태코드 체크!

API 테스트를 하기 위해서는 원하는 URL 주소로 요청(requests(get/post/put/delete)) 을 보내고, 서버에서는 그 요청을 받아 처리한 후 요청자에게 응답(response) 을 줍니다.

아래에 보이는 트렌비 이벤트 페이지를 예로 들어 API 상태를 체크해 보겠습니다.

API를 Request로 호출하면 위와 같은 상태코드를 응답 받을 수 있습니다.

이 상태 코드를 보고 요청이 잘 처리되었는지 문제가 있는지 알 수가 있습니다.

상태 코드는 응답 객체의 ‘status_code’ 를 통해 간단하게 얻을 수 있습니다.

상태 코드(status_code)

  • 1xx (정보): 요청을 받았으며 프로세스를 계속한다.
  • 2xx (성공): 요청을 성공적으로 받았으며 인식했고 수용하였다.
  • 3xx (리다이렉션): 요청 완료를 위해 추가 작업 조치가 필요하다.
  • 4xx (클라이언트 오류): 요청의 문법이 잘못되었거나 요청을 처리할 수 없다.
  • 5xx (서버 오류): 서버가 명백히 유효한 요청에 대해 충족을 실패했다.
import requests

# 이벤트 API
event_url = 'https://www.trenbe.com/event_url/****'

# event_url 요청 및 응답
res = requests.get(event_url)

# status_code 출력
print(res.status_code)

>> 정상  경우 2xx
>> 실패  경우 4xx or 5xx

위와 같이 간단한 코드로 API 상태를 체크 할 수 있었습니다.

이벤트 페이지 기준으로 UI를 포함한 테스트 시 약 40초가 소요 되었고, API만 테스트할 경우는 약 18초 정도로 절반 이상 빨랐습니다.

(실제 코드 실행 기준만 본다면 2~3초면 검수가 완료됩니다.)

두 번째. 상태 코드가 2xx이면 데이터들도 정상일까!??

첫 번째 이야기에서 상태 코드가 2xx일 경우 정상적으로 응답 받은 것을 알 수 있습니다.

하지만 응답은 정상적으로 왔지만 그 안에 포함되어 있는 데이터 들도 정상일까(?) 라는 의문이 들었고, 실제로 상태 코드가 2xx인 경우에도 데이터들이 정상적으로 오는지, 유효한 데이터 인지 확인 할 필요가 있었습니다.

트렌비 상품 페이지에서는 화면에 보이는 정보를 구성하기 위해 여러 API를 호출하고 있습니다. 아래 화면을 보면 보기에는 아무 문제가 없어 보이지만 상품 페이지가 정상적으로 표시됐다고 해서 모든 기능이 정상 동작 하고 있을까요??

보기에는 문제가 없어 보이더라도 만약 데이터를 제대로 불러오지 못했다면 일부 기능은 동작 시 오류가 발생할 수 있습니다.

상품 페이지 내에 중요 API 중 하나인 쿠폰 데이터를 예로 들겠습니다.

# 쿠폰 데이터 조회
def get_product_coupon():
    product_url = f'{base_url}/coupon*****/*********'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': token
    }
    payload = {
        "device":1,
        "orderItem":
        {
            "brandId":16963,
            "categoryId":0,
            "finalPrice":100000,
            "*******":"*******",
            "*********":****,
            "********":***
            }
        }
    response = requests.post(product_url, headers=headers, data=json.dumps(payload), timeout=1)

    return response.json()

먼저, 쿠폰 데이터를 검증하기 위해서 위와 같이 코드 작성을 하고 API를 호출합니다.

API가 정상적으로 호출 될 경우 JSON 형태의 응답을 받을 수 있습니다. JSON 형태로 받은 응답 값에서 호출한 값과 기대한 값을 비교하여 정상적으로 응답이 들어오는지 확인 할 수 있습니다.

쿠폰 사용 가능 일 수, 쿠폰 명, 쿠폰 타입 등

추가로 API의 응답 시간(속도) 을 검증하는 것도 중요했습니다.

예를들어 음료수 자판기에서 돈을 투입하고 음료를 선택했는데 10분뒤에 음료가 나온다고 생각해보면 다시는 그 자판기를 이용하지 않을 겁니다.

응답 시간을 확인하는 것은 생각보다 간단했습니다.

Requests에 timeout=N 값을 설정해서 제한시간 내에 응답이 오는지 확인 할 수 있었습니다.

response = requests.post(product_url, headers=headers, data=json.dumps(payload), timeout=1)

여기까지 API의 상태와 데이터들까지 확인하고 응답 속도까지 확인 할 수 있었습니다.

세 번째. 유저시나리오 기반의 API 테스트를 적용하자!

테스트를 자동화하려 할 때 어떤 케이스를 자동화하여 수행하면 효과적인지 판단하는 것도 매우 중요한 부분이라고 생각합니다.

저희는 로그인부터 주문까지의 유저 시나리오에 필요한 주요 API를 테스트에 먼저 적용해 보았습니다.

코드를 작성하기에 앞서 개발자들과 협의하여 API 목록을 식별하였고, 어떤식으로 자동화를 하면 좋을 지 함께 검토하였습니다.

운영중인 상품으로 테스트를 할 경우 데이터가 바뀌어 에러를 발생 시킬 수 있기 때문에, 지속적이고 반복적인 테스트를 위해 별도의 테스트 전용 상품을 생성하였습니다.

아래는 유저시나리오 기반의 핵심 API 리스트 중 일부입니다.

  • trenbe.com/login
  • trenbe.com/product
  • trenbe.com/order
  • trenbe.com/payment-init

가장 먼저 주문 정보를 가져오기 위해서는 로그인 정보 즉, 토큰 값을 필요로 하기에 아래와 같이 코드를 작성하였습니다.

# 로그인 정보 
def usr_token_get(email, password):
    login_url = f'{base_url}/login'
    headers = {
        'Content-Type': 'application/json'
    }
    payload = {
        'appInfo': '',
        'email': email,
        'password': password,
        'provider': 'trenbe',
        '*********': '***',
        '********': ****
    }
    response = requests.post(login_url, headers=headers, data=json.dumps(payload), timeout=1)
    return response.json()['Token']

로그인 정보를 가지고 오는데 성공하였다면, 상품 데이터를 조회합니다.

# 상품 데이터 조회
def product(goodsno):
    product_url = f'{base_url}/product/**********/******/****'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': token
    }
    response = requests.get(product_url, headers=headers, timeout=1)

    return response.json()

위에서 조회 한 로그인 정보와 상품 데이터를 이용하여 주문서로 이동하고 주문서 ID를 조회 할 수 있습니다.

주문서의 경우 주문서가 생성 될 때 마다 주문번호가 달라지기에 주문번호를 가져오는 코드가 필요 했습니다.

# 주문서 ID 조회
def get_order_id(product_data):
    get_order_url = f'{base_url}/order'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': token
    }
    payload = product_data
    response = requests.post(get_order_url, headers=headers, data=json.dumps(payload), timeout=1)

    return response.json()['id']

주문서에 필요한 각각의 API가 호출되고 데이터가 정상적으로 내려 오는지 확인을 하고 최종적으로 모든 결제 정보가 정상적으로 넘어 왔는지 확인을 합니다.

# 결제 정보 확인 (토스 가상계좌를 통한 주문 결제)
def order_payment_init(order_id):
    order_url = f'{base_url}/order/{order_id}/******'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': token
    }
    payload = {
        'paymentMethod': 'ta',
        'pg': 'TOSS_PAYMENTS'
    }
    response = requests.put(order_url, headers=headers, data=json.dumps(payload), timeout=1)

    return response.json()

아래와 같이 각각 API의 응답은 정상인지, 데이터들은 잘 넘어 오는지, 제한시간내에 정상적으로 응답 하는지 확인 할 수 있었습니다.

로그인부터 주문까지 유저 시나리오에 필요한 API들을 테스트 해보았습니다.

네 번째. Github Actions를 통하여 API 테스트 자동화 하기!

저희 트렌비는 Selenium을 통한 UI 테스트 자동화가 적용되어 있고, Github Actions를 통해 주기적으로 테스트를 실행시켜 확인 할 수 있도록 하였습니다.

마찬가지로 API 테스트도 Github Actions를 통해 테스트 자동화를 적용시켜 보았습니다.

정해진 저장소에 작성한 API 테스트 코드를 commit 하고, 워크플로우 파일을 생성합니다. 워크플로우는 yaml 파일을 통해 설정을 하고 .github/workflows 폴더 아래에 yaml 파일을 위치시킵니다.

trenbe_api_test.yml 을 통해 push or merge 될 때 마다 해당 워크플로우가 자동으로 실행되게 설정했고, workflow_dispatch를 통해서 수동 테스트도 가능하게 설정했습니다. cronjob을 통해서 원하는 시간에 반복해서 테스트를 수행 할 수도 있습니다.

워크플로우가 정상적으로 실행 되면 위와 같이 리스트에 노출 되는 것을 확인 할 수 있습니다. Run workflow를 통해서 테스트를 실행 할 수 있고, 가장 최근에 실행 된 작업을 보시면 테스트가 28초만에 완료 된 걸 볼 수 있습니다.

테스트 로그는 아래와 같습니다.

테스트 실패 시 슬랙 메신저와 연동하여 메세지를 통한 알림을 받을 수도 있습니다.

마치며

API 테스트를 활용할 줄 알면 단순히 화면을 보고, 테스트 시나리오대로 검증하는 역할을 넘어서 테스트 수행 시간 단축 및 리소스 감소를 통해 더욱 단순하고 빠르게 일을 해결 할 수 있습니다.

매뉴얼 테스트, UI 테스트 자동화, API 테스트 자동화를 적절히 분배하여 프로젝트를 검증하거나 배포 전 최종 검증 단계에서 테스트를 진행 할 경우 서비스의 품질을 좀 더 높일 수 있는 기회가 될 것으로 생각합니다. 👊

앞으로도 더 나은 서비스 품질을 위해 고민하고 노력하겠습니다.

감사합니다.