API contract testing validates that the Atom backend API conforms to its OpenAPI specification. We use Schemathesis for property-based contract testing and openapi-diff for breaking change detection.
- Schemathesis: Property-based API contract testing (built on Hypothesis)
- openapi-diff: Breaking change detection for OpenAPI specs
- FastAPI: Auto-generates OpenAPI spec at
/openapi.json
backend/
├── tests/
│ ├── contract/ # Contract test directory
│ │ ├── __init__.py
│ │ ├── conftest.py # Schemathesis fixtures
│ │ ├── test_core_api.py # Health, agent endpoints
│ │ ├── test_canvas_api.py # Canvas endpoints
│ │ └── test_governance_api.py # Governance endpoints
│ └── scripts/
│ ├── generate_openapi_spec.py # Generate OpenAPI spec
│ └── detect_breaking_changes.py # Breaking change detection
├── openapi.json # Baseline OpenAPI spec (committed)
└── docs/
└── API_CONTRACT_TESTING.md # This file
cd backend
pytest tests/contract/ -v -m contractcd backend
pytest tests/contract/test_core_api.py -vcd backend
python tests/scripts/generate_openapi_spec.py -o openapi.jsoncd backend
python tests/scripts/detect_breaking_changes.pyimport pytest
from conftest import schema
from hypothesis import settings
class TestMyEndpoint:
@schema.parametrize(endpoint="/api/v1/my-endpoint")
@settings(max_examples=20, deadline=None)
def test_my_endpoint_contracts(self, case):
response = case.call_and_validate()
# Schemathesis validates:
# - Response status code matches spec
# - Response body matches schema
# - Required headers presentFor protected endpoints, use the schema_with_auth fixture:
from conftest import schema_with_auth
@schema_with_auth.parametrize(endpoint="/api/v1/protected")
def test_protected_endpoint(self, case):
response = case.call_and_validate()The contract tests run automatically on every PR via .github/workflows/contract-tests.yml:
- Schemathesis Tests: Validates all API contracts
- Breaking Change Detection: Compares OpenAPI spec against baseline
- PR Comments: Reports breaking changes on PR
curl http://localhost:8001/openapi.json | python -c "import json, sys; d=json.load(sys.stdin); print(f'{len(d[\"paths\"])} endpoints')"cd backend && pytest tests/contract/ -v -m contract --collect-only | grep "test_" | wc -lcd backend && python tests/scripts/detect_breaking_changes.py --helpCheck .github/workflows/contract-tests.yml for pull_request trigger.
The workflow sets continue-on-error: false for contract tests.
# WRONG: Manual HTTP calls bypass Schemathesis validation
def test_health_endpoint(self):
with TestClient(app) as client:
response = client.get("/health")
assert response.status_code in [200, 400, 401, 403, 404] # Too loose!# CORRECT: Schemathesis validates schema automatically
@schema.parametrize(endpoint="/health")
@settings(max_examples=10, deadline=None)
def test_health_endpoint_contracts(self, case):
response = case.call_and_validate() # Validates schema!
assert response.status_code in [200, 503] # Business logic onlyKey differences:
- Use
@schema.parametrize()decorator - generates diverse test cases - Use
case.call_and_validate()- automatic schema validation - Remove loose status code assertions - Schemathesis handles it
- Hypothesis generates edge cases you wouldn't think of
# Run all contract tests
pytest tests/contract/ -v -m contract
# Run specific test file
pytest tests/contract/test_core_api.py -v
# Run with more examples (slower but more thorough)
pytest tests/contract/ -v -m contract --hypothesis-max-examples=100Ensure you're in the backend directory and dependencies are installed:
cd backend
pip install -r requirements.txtInstall openapi-diff via npx (auto-installed on first run):
npx openapi-diff --versionCheck for:
- Database differences (SQLite vs PostgreSQL)
- Environment variables
- FastAPI lifespan context issues
To catch breaking changes before pushing, add a pre-commit hook:
Create .git/hooks/pre-commit:
#!/bin/bash
# Run contract tests before committing
cd backend
pytest tests/contract/ -v -m contract --maxfail=5
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo ""
echo "❌ Contract tests failed. Commit aborted."
echo " Fix the issues or skip with: git commit --no-verify"
exit 1
fi
# Detect breaking changes
python3 tests/scripts/detect_breaking_changes.py
if [ $? -ne 0 ]; then
echo ""
echo "❌ Breaking changes detected. Commit aborted."
echo " To update baseline: python3 tests/scripts/generate_openapi_spec.py -o openapi.json"
echo " To skip: git commit --no-verify"
exit 1
fiMake it executable:
chmod +x .git/hooks/pre-commitThis ensures contract violations never leave your development machine.
- Schemathesis Documentation
- openapi-diff GitHub
- FastAPI OpenAPI Customization
- Phase 128 Research