Skip to main content

Generate OpenAPI documentation from pytest tests

Project description

pytest-swag

Generate OpenAPI documentation from pytest tests.

pytest-swag is a framework-agnostic pytest plugin that turns your existing API tests into living OpenAPI 3.0/3.1 documentation. Define your API spec inline with a fluent builder DSL, validate responses against it with jsonschema, and produce a complete OpenAPI document at the end of your test session.


English | 한국어


English

Installation

pip install pytest-swag

Optional extras:

pip install pytest-swag[yaml]       # YAML output support
pip install pytest-swag[requests]   # requests library adapter
pip install pytest-swag[dev]        # Development dependencies

Quick Start

def test_get_blog(swag):
    swag.path("/blogs/{id}").get("Retrieves a blog")
    swag.parameter("id", in_="path", schema={"type": "string"})
    swag.response(200, schema={
        "type": "object",
        "properties": {"id": {"type": "integer"}, "title": {"type": "string"}},
    })

    response = client.get("/blogs/1")
    swag.validate(response.status_code, response.json())

Run your tests with the --swag flag:

pytest --swag

This generates an openapi.json file containing your full API specification.

How It Works

  1. Define your API spec using the swag fixture's builder DSL
  2. Validate each response against the declared schema (jsonschema)
  3. Collect all validated operations across your test suite
  4. Generate a complete OpenAPI document at session end

Only tests that pass validation are included in the output. Failed tests are automatically excluded, keeping your documentation accurate.

Configuration

Via pyproject.toml

[tool.pytest-swag]
openapi = "3.1.0"
output_path = "docs/openapi.json"
output_format = "json"   # "json", "yaml", or "both"

[tool.pytest-swag.info]
title = "My API"
version = "1.0.0"

Via conftest.py fixture

import pytest

@pytest.fixture(scope="session")
def swag_config():
    return {
        "openapi": "3.1.0",
        "info": {"title": "My API", "version": "1.0.0"},
        "output_path": "docs/openapi.json",
        "output_format": "json",
        "servers": [{"url": "https://api.example.com/v1"}],
        "security": [{"BearerAuth": []}],
    }

Builder DSL Reference

Path & HTTP Methods

swag.path("/users").get("List users")
swag.path("/users").post("Create user")
swag.path("/users/{id}").put("Update user")
swag.path("/users/{id}").patch("Partial update")
swag.path("/users/{id}").delete("Delete user")

Parameters

# Path parameter (always required)
swag.parameter("id", in_="path", schema={"type": "integer"})

# Query parameter (optional by default)
swag.parameter("page", in_="query", schema={"type": "integer"})

# Required header
swag.parameter("X-Api-Key", in_="header", schema={"type": "string"}, required=True)

Request Body

swag.request_body(
    content_type="application/json",
    schema={
        "type": "object",
        "required": ["title"],
        "properties": {
            "title": {"type": "string"},
            "content": {"type": "string"},
        },
    },
)

Responses

# With schema
swag.response(200, description="OK", schema={
    "type": "object",
    "properties": {"id": {"type": "integer"}},
})

# Without schema (e.g. 204 No Content)
swag.response(204, description="Deleted")

# With $ref (requires swag_schemas fixture)
swag.response(200, schema={"$ref": "#/components/schemas/User"})

Tags & Security

swag.tag("Users")
swag.security("BearerAuth")

Validation

# Manual validation
swag.validate(response.status_code, response.json())

# Validates:
# 1. Status code is documented
# 2. Response body matches the declared schema (via jsonschema)

Component Schemas ($ref Support)

Define reusable schemas via the swag_schemas fixture:

@pytest.fixture(scope="session")
def swag_schemas():
    return {
        "User": {
            "type": "object",
            "required": ["id", "name"],
            "properties": {
                "id": {"type": "integer"},
                "name": {"type": "string"},
                "email": {"type": "string", "format": "email"},
            },
        },
        "Error": {
            "type": "object",
            "properties": {
                "message": {"type": "string"},
            },
        },
    }

Then reference them in your tests:

def test_get_user(swag):
    swag.path("/users/{id}").get("Get user")
    swag.parameter("id", in_="path", schema={"type": "integer"})
    swag.response(200, schema={"$ref": "#/components/schemas/User"})
    swag.response(404, schema={"$ref": "#/components/schemas/Error"})

    response = client.get("/users/1")
    swag.validate(response.status_code, response.json())

The $ref references are recursively resolved during validation and preserved as-is in the generated OpenAPI document.

Security Schemes

@pytest.fixture(scope="session")
def swag_security_schemes():
    return {
        "BearerAuth": {
            "type": "http",
            "scheme": "bearer",
            "bearerFormat": "JWT",
        },
        "ApiKeyAuth": {
            "type": "apiKey",
            "in": "header",
            "name": "X-API-Key",
        },
    }

Requests Adapter

For projects using the requests library, use the swag_requests fixture for automatic response extraction:

def test_list_users(swag_requests):
    swag_requests.path("/users").get("List users")
    swag_requests.response(200, schema={
        "type": "array",
        "items": {"$ref": "#/components/schemas/User"},
    })

    response = requests.get("http://localhost:8000/users")
    swag_requests.validate_response(response)
    # Automatically extracts status_code and JSON body from the response object

Multi-Document Output

Generate multiple OpenAPI documents from a single test suite using swag.doc():

@pytest.fixture(scope="session")
def swag_config():
    return [
        {"info": {"title": "Public API", "version": "1.0.0"}, "output_path": "docs/public.json"},
        {"info": {"title": "Admin API", "version": "1.0.0"}, "output_path": "docs/admin.json"},
    ]

def test_public_endpoint(swag):
    swag.doc("Public API")
    swag.path("/posts").get("List posts")
    swag.response(200, schema={"type": "array"})
    swag.validate(200, [])

def test_admin_endpoint(swag):
    swag.doc("Admin API")
    swag.path("/admin/users").get("List all users")
    swag.response(200, schema={"type": "array"})
    swag.validate(200, [])

CLI Options

Option Description
--swag Enable OpenAPI document generation
--swag-output PATH Override the output file path
--swag-dry-run Print the OpenAPI document to stdout instead of writing a file
--swag-no-output Run validation only, skip file generation
--swag-strict Warn when a test uses the swag fixture but never calls validate()

Full Example

# conftest.py
import pytest

@pytest.fixture(scope="session")
def swag_config():
    return {
        "openapi": "3.1.0",
        "info": {"title": "Blog API", "version": "1.0.0"},
        "servers": [{"url": "https://api.example.com/v1"}],
        "security": [{"BearerAuth": []}],
        "output_path": "docs/openapi.json",
        "output_format": "both",
    }

@pytest.fixture(scope="session")
def swag_schemas():
    return {
        "Blog": {
            "type": "object",
            "required": ["id", "title"],
            "properties": {
                "id": {"type": "integer"},
                "title": {"type": "string"},
                "content": {"type": "string"},
            },
        },
    }

@pytest.fixture(scope="session")
def swag_security_schemes():
    return {
        "BearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"},
    }
# test_blogs.py
def test_list_blogs(swag):
    swag.path("/blogs").get("List all blogs")
    swag.tag("Blogs")
    swag.parameter("page", in_="query", schema={"type": "integer"})
    swag.response(200, schema={
        "type": "array",
        "items": {"$ref": "#/components/schemas/Blog"},
    })

    response = client.get("/blogs")
    swag.validate(response.status_code, response.json())

def test_create_blog(swag):
    swag.path("/blogs").post("Create a blog")
    swag.tag("Blogs")
    swag.security("BearerAuth")
    swag.request_body(schema={
        "type": "object",
        "required": ["title"],
        "properties": {"title": {"type": "string"}, "content": {"type": "string"}},
    })
    swag.response(201, schema={"$ref": "#/components/schemas/Blog"})

    response = client.post("/blogs", json={"title": "Hello", "content": "World"})
    swag.validate(response.status_code, response.json())

def test_delete_blog(swag):
    swag.path("/blogs/{id}").delete("Delete a blog")
    swag.tag("Blogs")
    swag.parameter("id", in_="path", schema={"type": "integer"})
    swag.response(204, description="Deleted")

    response = client.delete("/blogs/1")
    swag.validate(response.status_code, None)
pytest --swag
# Generates docs/openapi.json and docs/openapi.yaml

Requirements

  • Python >= 3.10
  • pytest >= 7.0
  • jsonschema >= 4.0
  • PyYAML >= 6.0 (optional, for YAML output)

Acknowledgments

pytest-swag is inspired by rswag, the excellent Ruby/RSpec library for generating Swagger/OpenAPI documentation from integration tests. We are grateful to the rswag team for pioneering the "test-driven documentation" approach that bridges the gap between API testing and API documentation. pytest-swag brings this philosophy to the Python/pytest ecosystem.

License

MIT


한국어

소개

pytest-swag는 pytest 테스트 코드에서 OpenAPI 문서를 자동 생성하는 프레임워크 비의존 pytest 플러그인입니다. 빌더 DSL로 API 스펙을 정의하고, jsonschema로 응답을 검증한 뒤, 테스트 세션 종료 시 완전한 OpenAPI 문서를 출력합니다.

설치

pip install pytest-swag

선택적 추가 패키지:

pip install pytest-swag[yaml]       # YAML 출력 지원
pip install pytest-swag[requests]   # requests 라이브러리 어댑터
pip install pytest-swag[dev]        # 개발 의존성

빠른 시작

def test_get_blog(swag):
    swag.path("/blogs/{id}").get("블로그 조회")
    swag.parameter("id", in_="path", schema={"type": "string"})
    swag.response(200, schema={
        "type": "object",
        "properties": {"id": {"type": "integer"}, "title": {"type": "string"}},
    })

    response = client.get("/blogs/1")
    swag.validate(response.status_code, response.json())

--swag 플래그와 함께 테스트를 실행하세요:

pytest --swag

전체 API 스펙이 담긴 openapi.json 파일이 생성됩니다.

동작 원리

  1. swag fixture의 빌더 DSL로 API 스펙을 정의
  2. 각 응답을 선언된 스키마에 대해 jsonschema로 검증
  3. 테스트 스위트 전체에서 검증된 operation을 수집
  4. 세션 종료 시 완전한 OpenAPI 문서를 생성

검증에 통과한 테스트만 출력에 포함됩니다. 실패한 테스트는 자동으로 제외되어 문서의 정확성을 보장합니다.

설정

pyproject.toml로 설정

[tool.pytest-swag]
openapi = "3.1.0"
output_path = "docs/openapi.json"
output_format = "json"   # "json", "yaml", 또는 "both"

[tool.pytest-swag.info]
title = "My API"
version = "1.0.0"

conftest.py fixture로 설정

import pytest

@pytest.fixture(scope="session")
def swag_config():
    return {
        "openapi": "3.1.0",
        "info": {"title": "My API", "version": "1.0.0"},
        "output_path": "docs/openapi.json",
        "output_format": "json",
        "servers": [{"url": "https://api.example.com/v1"}],
        "security": [{"BearerAuth": []}],
    }

빌더 DSL 레퍼런스

경로 및 HTTP 메서드

swag.path("/users").get("사용자 목록")
swag.path("/users").post("사용자 생성")
swag.path("/users/{id}").put("사용자 수정")
swag.path("/users/{id}").patch("사용자 부분 수정")
swag.path("/users/{id}").delete("사용자 삭제")

파라미터

# 경로 파라미터 (항상 필수)
swag.parameter("id", in_="path", schema={"type": "integer"})

# 쿼리 파라미터 (기본: 선택)
swag.parameter("page", in_="query", schema={"type": "integer"})

# 필수 헤더
swag.parameter("X-Api-Key", in_="header", schema={"type": "string"}, required=True)

요청 본문

swag.request_body(
    content_type="application/json",
    schema={
        "type": "object",
        "required": ["title"],
        "properties": {
            "title": {"type": "string"},
            "content": {"type": "string"},
        },
    },
)

응답

# 스키마 포함
swag.response(200, description="OK", schema={
    "type": "object",
    "properties": {"id": {"type": "integer"}},
})

# 스키마 없음 (예: 204 No Content)
swag.response(204, description="삭제됨")

# $ref 참조 (swag_schemas fixture 필요)
swag.response(200, schema={"$ref": "#/components/schemas/User"})

태그 및 보안

swag.tag("Users")
swag.security("BearerAuth")

검증

# 수동 검증
swag.validate(response.status_code, response.json())

# 다음을 검증합니다:
# 1. 상태 코드가 문서화되어 있는지
# 2. 응답 본문이 선언된 스키마와 일치하는지 (jsonschema)

컴포넌트 스키마 ($ref 지원)

swag_schemas fixture로 재사용 가능한 스키마를 정의합니다:

@pytest.fixture(scope="session")
def swag_schemas():
    return {
        "User": {
            "type": "object",
            "required": ["id", "name"],
            "properties": {
                "id": {"type": "integer"},
                "name": {"type": "string"},
                "email": {"type": "string", "format": "email"},
            },
        },
        "Error": {
            "type": "object",
            "properties": {
                "message": {"type": "string"},
            },
        },
    }

테스트에서 참조:

def test_get_user(swag):
    swag.path("/users/{id}").get("사용자 조회")
    swag.parameter("id", in_="path", schema={"type": "integer"})
    swag.response(200, schema={"$ref": "#/components/schemas/User"})
    swag.response(404, schema={"$ref": "#/components/schemas/Error"})

    response = client.get("/users/1")
    swag.validate(response.status_code, response.json())

$ref 참조는 검증 시 재귀적으로 resolve되며, 생성된 OpenAPI 문서에는 원본 그대로 보존됩니다.

보안 스킴

@pytest.fixture(scope="session")
def swag_security_schemes():
    return {
        "BearerAuth": {
            "type": "http",
            "scheme": "bearer",
            "bearerFormat": "JWT",
        },
        "ApiKeyAuth": {
            "type": "apiKey",
            "in": "header",
            "name": "X-API-Key",
        },
    }

Requests 어댑터

requests 라이브러리를 사용하는 프로젝트에서는 swag_requests fixture로 응답을 자동 추출할 수 있습니다:

def test_list_users(swag_requests):
    swag_requests.path("/users").get("사용자 목록")
    swag_requests.response(200, schema={
        "type": "array",
        "items": {"$ref": "#/components/schemas/User"},
    })

    response = requests.get("http://localhost:8000/users")
    swag_requests.validate_response(response)
    # response 객체에서 status_code와 JSON 본문을 자동으로 추출합니다

멀티 문서 출력

swag.doc()을 사용하여 하나의 테스트 스위트에서 여러 OpenAPI 문서를 생성할 수 있습니다:

@pytest.fixture(scope="session")
def swag_config():
    return [
        {"info": {"title": "Public API", "version": "1.0.0"}, "output_path": "docs/public.json"},
        {"info": {"title": "Admin API", "version": "1.0.0"}, "output_path": "docs/admin.json"},
    ]

def test_public_endpoint(swag):
    swag.doc("Public API")
    swag.path("/posts").get("게시물 목록")
    swag.response(200, schema={"type": "array"})
    swag.validate(200, [])

def test_admin_endpoint(swag):
    swag.doc("Admin API")
    swag.path("/admin/users").get("전체 사용자 목록")
    swag.response(200, schema={"type": "array"})
    swag.validate(200, [])

CLI 옵션

옵션 설명
--swag OpenAPI 문서 생성 활성화
--swag-output PATH 출력 파일 경로 덮어쓰기
--swag-dry-run 파일 대신 stdout으로 OpenAPI 문서 출력
--swag-no-output 검증만 수행, 파일 생성 건너뛰기
--swag-strict swag fixture를 사용하지만 validate()를 호출하지 않은 테스트에 대해 경고

전체 예제

# conftest.py
import pytest

@pytest.fixture(scope="session")
def swag_config():
    return {
        "openapi": "3.1.0",
        "info": {"title": "Blog API", "version": "1.0.0"},
        "servers": [{"url": "https://api.example.com/v1"}],
        "security": [{"BearerAuth": []}],
        "output_path": "docs/openapi.json",
        "output_format": "both",
    }

@pytest.fixture(scope="session")
def swag_schemas():
    return {
        "Blog": {
            "type": "object",
            "required": ["id", "title"],
            "properties": {
                "id": {"type": "integer"},
                "title": {"type": "string"},
                "content": {"type": "string"},
            },
        },
    }

@pytest.fixture(scope="session")
def swag_security_schemes():
    return {
        "BearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"},
    }
# test_blogs.py
def test_list_blogs(swag):
    swag.path("/blogs").get("블로그 목록")
    swag.tag("Blogs")
    swag.parameter("page", in_="query", schema={"type": "integer"})
    swag.response(200, schema={
        "type": "array",
        "items": {"$ref": "#/components/schemas/Blog"},
    })

    response = client.get("/blogs")
    swag.validate(response.status_code, response.json())

def test_create_blog(swag):
    swag.path("/blogs").post("블로그 생성")
    swag.tag("Blogs")
    swag.security("BearerAuth")
    swag.request_body(schema={
        "type": "object",
        "required": ["title"],
        "properties": {"title": {"type": "string"}, "content": {"type": "string"}},
    })
    swag.response(201, schema={"$ref": "#/components/schemas/Blog"})

    response = client.post("/blogs", json={"title": "Hello", "content": "World"})
    swag.validate(response.status_code, response.json())

def test_delete_blog(swag):
    swag.path("/blogs/{id}").delete("블로그 삭제")
    swag.tag("Blogs")
    swag.parameter("id", in_="path", schema={"type": "integer"})
    swag.response(204, description="삭제됨")

    response = client.delete("/blogs/1")
    swag.validate(response.status_code, None)
pytest --swag
# docs/openapi.json과 docs/openapi.yaml이 생성됩니다

요구 사항

  • Python >= 3.10
  • pytest >= 7.0
  • jsonschema >= 4.0
  • PyYAML >= 6.0 (선택, YAML 출력용)

감사의 말

pytest-swag는 Ruby/RSpec 기반의 Swagger/OpenAPI 문서 자동 생성 라이브러리인 rswag에서 영감을 받았습니다. 테스트와 API 문서 사이의 간극을 잇는 "테스트 주도 문서화(test-driven documentation)" 접근법을 개척한 rswag 팀에 깊은 감사를 드립니다. pytest-swag는 이 철학을 Python/pytest 생태계로 가져옵니다.

라이선스

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_swag-0.1.0.tar.gz (34.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_swag-0.1.0-py3-none-any.whl (14.7 kB view details)

Uploaded Python 3

File details

Details for the file pytest_swag-0.1.0.tar.gz.

File metadata

  • Download URL: pytest_swag-0.1.0.tar.gz
  • Upload date:
  • Size: 34.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytest_swag-0.1.0.tar.gz
Algorithm Hash digest
SHA256 62f1f76f2a70b742f3117aec62e5d8b408f075aa0e3d3e81f40d5efeaae25fef
MD5 99276431b7a56f944899e3605fd26f8f
BLAKE2b-256 51d0c49238f3580699308aed20a1a9bbe25d6a90841099c54a96a787ef8d4bd2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_swag-0.1.0.tar.gz:

Publisher: publish.yml on builder-shin/pytest-swag

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytest_swag-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pytest_swag-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 14.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytest_swag-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c013e87ba52eb61c109949c224a7102c80989b1f3fc0a1d8a62cee31d607356e
MD5 3f4d0a64c77adae00414340cccfa8933
BLAKE2b-256 1e1449766ddd4604d8ce2fe84528e57adc753ca94e5d53d41ff5a14a3c98843b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_swag-0.1.0-py3-none-any.whl:

Publisher: publish.yml on builder-shin/pytest-swag

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page