Pytest Exporter
Finding API bugs in an interactive client is great, but ensuring they don't regress in continuous integration is essential.
SpecForge bridges interactive discovery with automated CI pipelines via the One-Click Pytest Exporter.
How It Works
Whenever SpecForge plans or executes boundary tests for an endpoint, you can export those cases directly into valid, standalone Python test files.
In the UI
Inside the Automated Contract Tests panel of any endpoint, click the Export Pytest button. SpecForge will instantly generate and download a clean test_api_contracts.py file.
Programmatically
You can also generate pytest code from your Python scripts or CI jobs using the PytestExporter:
from specforge.parser.openapi3 import OpenAPIParser
from specforge.testing.planner import TestPlanner
from specforge.testing.exporters.pytest_export import PytestExporter
# 1. Parse OpenAPI Spec
parser = OpenAPIParser(spec_dict)
spec = parser.parse()
# 2. Pick an operation and plan boundary tests
operation = spec.paths["/users"].operations["post"]
test_plan = TestPlanner.plan_operation_tests(operation)
# 3. Export to Pytest code
code = PytestExporter.export_operation_tests(
test_plan=test_plan,
base_url="http://localhost:8000"
)
# 4. Save to test directory
with open("tests/test_users_contracts.py", "w") as f:
f.write(code)
Sample Generated Pytest Code
The generated file is 100% standard pytest code using httpx and @pytest.mark.parametrize:
"""Automated Contract and Invariant Tests generated by SpecForge."""
import pytest
import httpx
BASE_URL = "http://localhost:8000"
TEST_CASES = [
{
"id": "whitespace_username",
"description": "Reject whitespace-only username",
"payload": {"username": " ", "age": 25},
"expected_status": 422,
},
{
"id": "missing_required_age",
"description": "Reject payload when required field 'age' is missing",
"payload": {"username": "alice"},
"expected_status": 422,
},
{
"id": "negative_age",
"description": "Reject age below minimum 0",
"payload": {"username": "alice", "age": -1},
"expected_status": 422,
},
]
@pytest.mark.parametrize("case", TEST_CASES, ids=lambda c: c["id"])
def test_post_users_invariants(case):
"""Enforce Zero 500s invariant against schema boundary violations."""
with httpx.Client(base_url=BASE_URL, timeout=5.0) as client:
response = client.post("/users", json=case["payload"])
# Zero 500s Invariant: Must NEVER crash with 500
assert response.status_code != 500, (
f"Zero 500s Invariant Failed! Edge case '{case['description']}' "
f"caused unhandled 500 crash."
)
assert response.status_code == case["expected_status"]