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
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
- Define your API spec using the
swagfixture's builder DSL - Validate each response against the declared schema (jsonschema)
- Collect all validated operations across your test suite
- 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 파일이 생성됩니다.
동작 원리
swagfixture의 빌더 DSL로 API 스펙을 정의- 각 응답을 선언된 스키마에 대해 jsonschema로 검증
- 테스트 스위트 전체에서 검증된 operation을 수집
- 세션 종료 시 완전한 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62f1f76f2a70b742f3117aec62e5d8b408f075aa0e3d3e81f40d5efeaae25fef
|
|
| MD5 |
99276431b7a56f944899e3605fd26f8f
|
|
| BLAKE2b-256 |
51d0c49238f3580699308aed20a1a9bbe25d6a90841099c54a96a787ef8d4bd2
|
Provenance
The following attestation bundles were made for pytest_swag-0.1.0.tar.gz:
Publisher:
publish.yml on builder-shin/pytest-swag
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_swag-0.1.0.tar.gz -
Subject digest:
62f1f76f2a70b742f3117aec62e5d8b408f075aa0e3d3e81f40d5efeaae25fef - Sigstore transparency entry: 1283001443
- Sigstore integration time:
-
Permalink:
builder-shin/pytest-swag@ed4aeb3a6f3dc7a9fbfa4608b71b0e2d09f57fd8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/builder-shin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ed4aeb3a6f3dc7a9fbfa4608b71b0e2d09f57fd8 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c013e87ba52eb61c109949c224a7102c80989b1f3fc0a1d8a62cee31d607356e
|
|
| MD5 |
3f4d0a64c77adae00414340cccfa8933
|
|
| BLAKE2b-256 |
1e1449766ddd4604d8ce2fe84528e57adc753ca94e5d53d41ff5a14a3c98843b
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_swag-0.1.0-py3-none-any.whl -
Subject digest:
c013e87ba52eb61c109949c224a7102c80989b1f3fc0a1d8a62cee31d607356e - Sigstore transparency entry: 1283001448
- Sigstore integration time:
-
Permalink:
builder-shin/pytest-swag@ed4aeb3a6f3dc7a9fbfa4608b71b0e2d09f57fd8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/builder-shin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ed4aeb3a6f3dc7a9fbfa4608b71b0e2d09f57fd8 -
Trigger Event:
push
-
Statement type: