Skip to content

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