3.7 KiB
3.7 KiB
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