Python Testing Standards¶
Status: 🟢 Active | Owner: Python Guild
Framework and Required Tools¶
| Tool | Purpose |
|---|---|
pytest | Test runner — required for all Python projects |
pytest-cov | Coverage reporting |
pytest-asyncio | Async test support |
factory_boy | Test fixture factories |
httpx TestClient / AsyncClient | FastAPI integration testing |
testcontainers-python | Real database integration tests |
Configuration¶
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "--strict-markers --tb=short -q"
markers = [
"unit: fast, isolated unit tests",
"integration: tests that use real infrastructure",
"e2e: end-to-end tests",
]
[tool.coverage.run]
source = ["src"]
omit = ["*/migrations/*", "*/tests/*"]
[tool.coverage.report]
fail_under = 80
Test File Structure¶
tests/
├── unit/
│ ├── domain/
│ │ └── test_order.py
│ └── application/
│ └── test_create_order.py
├── integration/
│ ├── test_order_repository.py
│ └── conftest.py # shared fixtures (DB, containers)
└── e2e/
└── test_order_api.py
Writing Unit Tests¶
# tests/unit/domain/test_order.py
import pytest
from acme.domain.order import Order, OrderStatus
class TestOrder:
def test_new_order_has_pending_status(self):
order = Order.create(customer_id="cust-1", items=[])
assert order.status == OrderStatus.PENDING
def test_confirm_order_changes_status(self):
order = Order.create(customer_id="cust-1", items=[sample_item()])
order.confirm()
assert order.status == OrderStatus.CONFIRMED
def test_cannot_confirm_empty_order(self):
order = Order.create(customer_id="cust-1", items=[])
with pytest.raises(ValueError, match="Cannot confirm an empty order"):
order.confirm()
Integration Tests with Testcontainers¶
# tests/integration/conftest.py
import pytest
from testcontainers.postgres import PostgresContainer
from sqlalchemy.ext.asyncio import create_async_engine
@pytest.fixture(scope="session")
def postgres():
with PostgresContainer("postgres:16-alpine") as pg:
yield pg
@pytest.fixture(scope="session")
async def engine(postgres):
engine = create_async_engine(postgres.get_connection_url())
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
await engine.dispose()
FastAPI Integration Testing¶
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.mark.asyncio
async def test_create_order_returns_201():
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as client:
response = await client.post("/orders", json={
"customer_id": "cust-abc",
"items": [{"sku": "WIDGET-1", "quantity": 2}],
})
assert response.status_code == 201
assert response.json()["status"] == "PENDING"
Coverage Requirements¶
| Test Type | Coverage Requirement |
|---|---|
| Domain / application layer | ≥ 90% line coverage |
| Adapters (HTTP, DB) | ≥ 80% line coverage |
| Overall project | ≥ 80% line coverage |
References¶
Last reviewed: 2025-Q4 | Owner: Python Guild