Files
openapi-first/docs/wiki/07_testing.md

3.7 KiB
Raw Permalink Blame History

Testing — Smoke-First, Contract-First

Everything openapi-first ships is tested against real specs, real handlers, and real HTTP through FastAPI's TestClient and the bundled templates. There are no fakes of the library itself.


🧪 1. Suite Overview

tests/
├── conftest.py        # fixtures: spec_file, routes_module, app, client
├── test_app.py        # OpenAPIFirstApp: routes served, overrides, fail-fast
├── test_binder.py     # bind_routes: opId resolution + missing-handler failures
├── test_loader.py     # load_openapi: json/yaml, env resolution, validation
└── test_client.py     # OpenAPIClient: opId→callable, params, error cases

Run with:

pytest            # 23 tests, zero mocks of the library
pytest -q
pytest tests/test_loader.py

🏗️ 2. The Fixture Pattern

# conftest.py (abridged)
@pytest.fixture
def spec_file(tmp_path):
    path = tmp_path / "openapi.json"
    path.write_text(json.dumps(SPEC), encoding="utf-8")
    return str(path)

@pytest.fixture
def app(spec_file):
    return OpenAPIFirstApp(
        openapi_path=spec_file,
        routes_module=routes_module(),
    )

@pytest.fixture
def client(spec_file):
    return OpenAPIClient(json.loads(Path(spec_file).read_text()))

🧪 3. What's Actually Asserted

3.1 App-level smoke

def test_app_routes_served(spec_file):
    app = OpenAPIFirstApp(openapi_path=spec_file, routes_module=routes)
    client = TestClient(app)
    assert client.get("/health").json() == {"status": "ok"}

3.2 Fail-fast (the heart)

def test_app_missing_handler_fails_at_startup(spec_file):
    with pytest.raises(MissingOperationHandler):
        OpenAPIFirstApp(openapi_path=spec_file, routes_module=empty_routes)

3.3 Loader validation

def test_loader_invalid_spec_fails():
    with pytest.raises(OpenAPISpecLoadError):
        load_openapi("broken.yaml")

3.4 Client contract drift

def test_client_duplicate_operation_id_raises():
    with pytest.raises(OpenAPIClientError):
        OpenAPIClient(dup_spec)

🏛️ 4. Testing the Templates

Each bundled template ships its own test + in-memory store, so you get a runnable contract test the moment you scaffold:

openapi-first scaffold crud_app my-service
cd my-service
pytest -q

test_crud_app.py / test_model_app.py / test_vet_app.py exercise the full CRUD surface through TestClient — including 201/204 status codes, 404s, and (in vet_app) SSE streaming via StreamingResponse.


🕹️ 5. Quality Gates (CI)

The same gates the library itself must pass are what keep the docs honest:

Gate Purpose
black --check formatting parity
ruff check lint hygiene
mypy type safety (strict, --disable-error-code where intentional)
pytest 23 tests, green
coverage tracked via pytest-cov (HTML + XML + term)

Run the whole gate locally:

black --check openapi_first tests
ruff check openapi_first tests
mypy openapi_first
pytest

💡 6. Testing Tips

  • Start from startup: assert OpenAPIFirstApp(...) raises for the broken contracts — those are your most valuable tests
  • Client ↔ server: smoke a client against the same spec the app was built from — one spec, two sides, zero drift
  • Templates are scaffolds: their tests are copyable starting points, not canonical suites
  • No mocking of the library: exercise loader → binder → app → client as a real pipeline