Pytest API 자동화 테스트 프레임워크: 실전 튜토리얼

INEZA Felin-Michel

INEZA Felin-Michel

22 May 2026

Pytest API 자동화 테스트 프레임워크: 실전 튜토리얼

Apidog 엔터프라이즈

온프레미스 배포

SSO & RBAC

SOC 2 준수

Apidog Enterprise 살펴보기

Python 개발자들은 pytest가 불필요한 복잡성을 제거해주기 때문에 이를 사용합니다. 테스트는 단순히 test_로 시작하는 이름의 함수이고, 단언(assertion)은 일반적인 assert 문이며, 테스트 러너가 나머지를 처리합니다. requests 라이브러리와 함께 사용하면, 번거로운 절차 없이 API 테스트를 자동화하기 위한 완전한 코드 중심 프레임워크를 갖게 됩니다.

이 튜토리얼은 실제 pytest API 테스트 스위트를 구축하는 방법을 보여줍니다. 프로젝트를 설정하고, 첫 번째 요청 테스트를 작성하고, 픽스처(fixtures)를 사용하여 설정 로직을 공유하고, parametrize를 사용하여 여러 입력에 대해 동일한 테스트를 실행하고, 응답 상태, 본문 및 JSON 스키마에 대해 단언하는 방법을 배웁니다. 모든 예제는 현실적인 공개 API를 사용하므로 코드를 직접 적용할 수 있습니다.

프로젝트 설정

필요한 두 라이브러리를 가상 환경에 설치하세요:

python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema

깔끔한 레이아웃은 스위트가 커질수록 유지보수성을 높여줍니다:

api-tests/
  conftest.py        # 공유 픽스처
  test_users.py      # 사용자 엔드포인트 테스트
  test_orders.py     # 주문 엔드포인트 테스트
  pytest.ini         # 설정

Pytest는 테스트를 자동으로 탐지합니다. 파일은 test_로 시작하거나 _test.py로 끝나야 하며, 함수는 test_로 시작해야 하고, 테스트 클래스는 Test로 시작하고 __init__ 메서드가 없어야 합니다. 이 규칙들을 따르면 수동으로 탐지 설정을 할 필요가 없습니다. 자동화된 테스트라는 분야가 처음이라면, 자동화된 테스트란 무엇인가에 대한 저희의 입문서가 배경 지식을 제공합니다.

특히 API 테스트에 pytest를 사용하는 이유는 무엇일까요? requests 라이브러리가 HTTP를 처리하고, pytest는 그 주변의 모든 것을 처리합니다: 탐지, 가독성 높은 실패 출력을 가진 단언, 픽스처를 통한 설정 및 해제, parametrize를 통한 데이터 기반 실행, 그리고 보고. 팀에서 애플리케이션 자체에 이미 사용하고 있을 언어로, 작고 잘 문서화된 두 라이브러리로부터 완전한 API 자동화 프레임워크를 구축할 수 있습니다. 이러한 근접성은 중요합니다. 코드와 동일한 저장소에 있는 테스트는 정직하게 유지됩니다. 왜냐하면 주요 변경 사항과 그로 인한 실패 테스트가 동일한 풀 리퀘스트에 나타나기 때문입니다.

첫 번째 API 테스트 작성

pytest API 테스트는 요청을 보내고 응답에 대해 단언합니다. 다음은 사용자 엔드포인트에 대한 테스트입니다:

import requests

BASE_URL = "https://api.example.com/v1"

def test_get_user_returns_200():
    response = requests.get(f"{BASE_URL}/users/42")
    assert response.status_code == 200

def test_get_user_returns_expected_fields():
    response = requests.get(f"{BASE_URL}/users/42")
    body = response.json()
    assert body["id"] == 42
    assert "email" in body
    assert body["status"] == "active"

pytest -v로 스위트를 실행하세요. 실패하는 각 assert는 실제 값을 보여주는 상세 보고서를 생성하는데, 이는 pytest의 가장 뛰어난 기능 중 하나입니다. 특별한 단언 메서드가 필요 없습니다. 프레임워크는 일반 assert 문을 다시 작성하여 풍부한 출력을 제공합니다. 응답에 대해 수행할 가치가 있는 더 광범위한 검사에 대해서는 API 단언에 대한 저희 가이드를 참조하세요.

픽스처로 설정 공유

모든 테스트에서 기본 URL과 HTTP 세션을 반복하는 것은 비효율적입니다. 픽스처가 이 문제를 해결합니다. 픽스처는 @pytest.fixture로 데코레이트된 함수로, 테스트가 매개변수로 이름을 지정하여 요청할 수 있는 값을 생성합니다.

모든 테스트 파일이 가져오기 없이 사용할 수 있도록 공유 픽스처를 conftest.py에 배치하세요:

# conftest.py
import pytest
import requests

BASE_URL = "https://api.example.com/v1"

@pytest.fixture(scope="session")
def api_session():
    session = requests.Session()
    session.headers.update({"Accept": "application/json"})
    yield session
    session.close()

@pytest.fixture
def auth_token(api_session):
    response = api_session.post(
        f"{BASE_URL}/auth/login",
        json={"email": "qa@example.com", "password": "test-pass"},
    )
    return response.json()["token"]

scope="session" 인수는 세션이 각 테스트마다 생성되는 대신 전체 실행에 대해 한 번만 생성됨을 의미합니다. yield 키워드는 설정과 해제를 분리합니다. yield 이전의 코드가 먼저 실행되고, 픽스처가 범위를 벗어날 때 이후 코드가 실행됩니다. 테스트는 단순히 필요한 것을 요청합니다:

def test_create_order(api_session, auth_token):
    response = api_session.post(
        f"{BASE_URL}/orders",
        headers={"Authorization": f"Bearer {auth_token}"},
        json={"product_id": 7, "quantity": 2},
    )
    assert response.status_code == 201
    assert response.json()["status"] == "pending"

픽스처는 pytest의 오래된 setup_functionteardown_function 스타일을 대체하는 현대적인 방식입니다. 이들은 깔끔하게 구성되고, 스코프를 지원하며, 의존성을 명확하게 만듭니다. 이것이 공식 pytest 픽스처 문서에서 기본 접근 방식으로 권장하는 이유입니다.

여러 입력에 대해 하나의 테스트 실행

API 엔드포인트는 일반적으로 유효한 값, 유효하지 않은 값, 엣지 케이스 등 여러 입력에 대해 확인해야 합니다. 각각에 대해 별도의 함수를 작성하는 것은 지루합니다. @pytest.mark.parametrize 데코레이터는 하나의 테스트 본문을 입력 목록에 대해 실행합니다:

import pytest
import requests

BASE_URL = "https://api.example.com/v1"

@pytest.mark.parametrize("user_id,expected_status", [
    (42, 200),
    (99999, 404),
    (0, 404),
    (-1, 400),
])
def test_get_user_status_codes(api_session, user_id, expected_status):
    response = api_session.get(f"{BASE_URL}/users/{user_id}")
    assert response.status_code == expected_status

이것은 하나의 함수에서 네 개의 개별 테스트 케이스를 생성합니다. 각각은 독립적으로 실행되고 보고되므로, 하나의 잘못된 입력이 다른 입력들을 가리지 않습니다. Parametrize는 pytest의 데이터 기반 테스트에 대한 내장된 해답입니다. 입력 세트가 많아지면 파일에서 로드하는 대신; CSV 및 JSON을 사용한 데이터 기반 API 테스트에 대한 저희 가이드가 해당 패턴을 다룹니다. 각 입력이 어떤 상태 코드를 반환해야 하는지 확실하지 않다면, REST API가 사용해야 할 HTTP 상태 코드에 대한 참조가 유용한 동반자가 될 것입니다.

응답 본문 및 스키마 단언

상태 코드는 필요하지만 충분하지 않습니다. 형식이 잘못된 본문을 가진 200 응답은 여전히 버그입니다. 파싱된 JSON에 대해 직접 단언하세요:

def test_order_response_shape(api_session, auth_token):
    response = api_session.post(
        f"{BASE_URL}/orders",
        headers={"Authorization": f"Bearer {auth_token}"},
        json={"product_id": 7, "quantity": 2},
    )
    body = response.json()
    assert isinstance(body["id"], int)
    assert body["quantity"] == 2
    assert body["total"] > 0
    assert response.elapsed.total_seconds() < 1.0

더 강력한 보장을 위해 JSON 스키마에 대해 본문을 검증하세요. 이는 수동으로 작성된 검사가 놓칠 수 있는 이름 변경이나 누락된 필드와 같은 구조적 변화를 잡아냅니다:

from jsonschema import validate

order_schema = {
    "type": "object",
    "required": ["id", "product_id", "quantity", "status", "total"],
    "properties": {
        "id": {"type": "integer"},
        "product_id": {"type": "integer"},
        "quantity": {"type": "integer", "minimum": 1},
        "status": {"type": "string"},
        "total": {"type": "number"},
    },
}

def test_order_matches_schema(api_session, auth_token):
    response = api_session.post(
        f"{BASE_URL}/orders",
        headers={"Authorization": f"Bearer {auth_token}"},
        json={"product_id": 7, "quantity": 2},
    )
    validate(instance=response.json(), schema=order_schema)

스키마 유효성 검사는 하나의 스키마가 전체 응답 형태를 다루기 때문에 필드별 단언보다 확장성이 좋습니다. jsonschema 라이브러리는 표준적인 선택이며, 해당 유효성 검사 문서는 지원되는 키워드를 설명합니다.

CI에서 스위트 실행

pytest 스위트는 자동으로 실행될 때 그 가치를 발휘합니다. pytest는 실패 시 0이 아닌 종료 코드를 반환하는데, 이는 CI 서버가 빌드를 실패시키기 위해 정확히 필요한 것입니다. 인라인 표시를 위해 JUnit 보고서를 생성하세요:

pytest -v --junitxml=results.xml

이 명령을 GitHub Actions 단계 또는 다른 파이프라인에 연결하면 API 테스트가 모든 커밋을 게이트합니다. CI/CD 파이프라인의 API 테스트에 대한 저희 가이드는 토큰용 비밀 주입 및 환경 선택을 포함한 전체 설정을 보여줍니다.

두 가지 CI 습관은 pytest 스위트를 신뢰할 수 있게 유지합니다. 첫째, 테스트 파일에 비밀 정보나 환경 URL을 하드코딩하지 마십시오. 환경 변수에서 읽어오도록 하여 동일한 스위트가 CI의 스테이징 환경과 로컬에서 수정 없이 실행되도록 하세요:

import os

BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")

둘째, 피드백을 빠르게 유지하기 위해 독립적인 테스트를 병렬로 실행하세요. pytest-xdist 플러그인은 pytest -n auto 명령으로 테스트를 CPU 코어에 분산시킵니다. 병렬 실행은 테스트가 상태를 공유하지 않을 때만 작동하며, 이는 테스트 데이터 계층이 중요한 또 다른 이유입니다. 실행 순서에 의존하는 스위트는 병렬로 실행되는 순간 예측할 수 없게 실패할 것입니다.

pytest 스위트 유지보수성 유지

테스트 50개짜리 스위트는 쉽습니다. 테스트 500개짜리 스위트는 규율이 필요합니다. 세 가지 관행이 pytest API 프레임워크가 성장함에 따라 건전하게 유지되도록 합니다.

관련 테스트를 모듈로 그룹화하고, 장식을 위해서가 아니라 설정을 공유할 때만 클래스를 사용하십시오. 명확한 함수 세트를 가진 test_orders.py 파일이 하나의 거대한 파일보다 읽기 좋습니다. pytest.ini에 등록된 마크를 사용하여 테스트에 태그를 지정하여 서브셋을 실행할 수 있습니다: 빠른 게이트를 위한 @pytest.mark.smoke, 전체 검사를 위한 @pytest.mark.slow. 모든 커밋에서 스모크 테스트를 실행하고, 매일 밤 전체 테스트를 실행하십시오.

구성 중앙화. 기본 URL, 스키마 및 공유 픽스처는 conftest.py 또는 작은 구성 모듈에 속하며, 파일 간에 복사하여 붙여넣지 마십시오. 스테이징 URL이 변경될 때 한 줄만 편집해야 합니다. 자동화된 테스트 스크립트 작성에 대한 저희 가이드에서 다루는 모든 프레임워크에 적용되는 동일한 모듈화 원칙이 여기에도 적용됩니다: 두 번 작성하는 모든 것을 픽스처나 헬퍼로 추출하십시오.

대신 플랫폼을 선택해야 할 때

pytest 프레임워크는 팀이 Python을 사용하고 애플리케이션 코드 옆에 테스트를 두려는 경우 탁월합니다. QA 또는 제품 담당자가 기여해야 할 때, 또는 접착 코드를 유지보수하지 않고 한 곳에서 테스트 설계, 모의(mocking) 및 실행을 원할 때는 덜 편리합니다.

Apidog는 이러한 격차를 해소합니다. 수동으로 픽스처 및 단언 코드를 작성할 필요 없이 시각적 테스트 빌딩, OpenAPI 사양에 대한 스키마 유효성 검사, CSV 및 JSON을 통한 데이터 기반 실행, 그리고 CI를 위한 CLI 러너를 제공합니다. 많은 팀이 두 가지를 모두 사용합니다: 로직이 복잡한 시나리오에는 pytest를, 광범위한 커버리지를 위해 그리고 pytest 스위트가 테스트할 API를 설계하고 모의하는 데는 Apidog를 사용합니다. 오후 한나절이면 Apidog를 다운로드하여 실제 엔드포인트에서 두 가지 접근 방식을 비교해 볼 수 있습니다.

자주 묻는 질문

API 테스트를 위해 Python의 내장 unittest 대신 pytest를 사용하는 이유는 무엇인가요?

pytest는 상용구(boilerplate)가 덜 필요합니다. 테스트는 일반 함수이고, 단언은 풍부한 실패 출력을 가진 일반 assert 문이며, 픽스처는 unittest의 클래스 기반 메서드보다 더 유연하게 설정을 처리합니다. pytest는 또한 대규모 플러그인 생태계와 데이터 기반 테스트를 위한 내장 parametrize를 가지고 있습니다. 기존 unittest 스타일 테스트도 여전히 실행할 수 있으므로 마이그레이션 위험이 낮습니다.

픽스처와 parametrize의 차이점은 무엇인가요?

픽스처는 HTTP 세션이나 인증 토큰과 같은 재사용 가능한 리소스를 이를 요청하는 모든 테스트에 제공합니다. Parametrize는 동일한 테스트 본문을 여러 다른 입력 값에 대해 여러 번 실행합니다. 픽스처는 설정을 공유하고, parametrize는 케이스를 늘립니다. 이들은 잘 결합됩니다: parametrize된 테스트도 픽스처에 의존할 수 있습니다.

pytest API 테스트에서 응답 시간에 대해 단언해야 하나요?

response.elapsed.total_seconds()를 사용하여 할 수 있으며, 느슨한 상한선은 큰 회귀를 잡아냅니다. 하지만 pytest는 기능 테스트 도구이지, 부하 테스트 도구가 아닙니다. 실제 성능 작업을 위해서는 전용 도구를 사용하십시오. 정상적인 네트워크 변화가 불안정한 실패를 유발하지 않도록 시간 측정 단언을 관대하게 유지하십시오.

pytest에서 API 테스트를 독립적으로 유지하려면 어떻게 해야 하나요?

각 테스트에 리소스를 생성하고 정리하는 픽스처를 통해 자체 데이터를 제공하고, 테스트 실행 순서에 의존하지 마십시오. Pytest는 기본적으로 파일 순서로 테스트를 실행하지만, 잘 설계된 스위트는 이에 의존하지 않습니다. 독립적인 테스트는 병렬로 실행되고 개별적으로 실패할 수 있으므로 디버깅이 훨씬 쉬워집니다.

pytest는 OpenAPI 사양에 대해 응답을 검증할 수 있나요?

pytest 자체는 그렇지 않지만, jsonschema 라이브러리를 사용하여 JSON 스키마에 대해 유효성을 검사할 수 있으며, OpenAPI 문서에 대해 응답을 확인하는 플러그인도 존재합니다. 스키마 유효성 검사가 워크플로우의 핵심이라면, OpenAPI 사양에 대해 자동으로 유효성을 검사하는 Apidog와 같은 플랫폼이 플러그인 설정을 절약해 줄 수 있습니다.

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요