Python Testing Patterns
pytest Setup
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = ["--strict-markers", "-ra"]
[tool.coverage.run]
source = ["app"]
omit = ["tests/*", "*/migrations/*"]
[tool.coverage.report]
fail_under = 80
Fixture Patterns
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from httpx import AsyncClient, ASGITransport
# Scope: session for expensive setup, function for isolation
@pytest.fixture(scope="session")
async def engine():
engine = create_async_engine("postgresql+asyncpg://test:test@localhost/test_db")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest.fixture
async def db(engine) -> AsyncGenerator[AsyncSession, None]:
async with AsyncSession(engine) as session:
yield session
await session.rollback() # isolation between tests
@pytest.fixture
async def client(app) -> AsyncGenerator[AsyncClient, None]:
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
yield ac
# Factory fixtures
@pytest.fixture
def user_factory(db):
async def _create(**kwargs) -> User:
defaults = {"email": "test@example.com", "name": "Test User"}
return await user_service.create(db, {**defaults, **kwargs})
return _create
Parametrize Patterns
@pytest.mark.parametrize("email,expected", [
("valid@example.com", True),
("invalid-email", False),
("", False),
("a@b.c", True),
])
def test_email_validation(email: str, expected: bool) -> None:
assert is_valid_email(email) == expected
# Parametrize with IDs for readable output
@pytest.mark.parametrize("status_code,expected_error", [
pytest.param(400, "bad_request", id="bad-request"),
pytest.param(404, "not_found", id="not-found"),
pytest.param(422, "validation_error", id="validation"),
], ids=...)
Async Tests
# asyncio_mode = "auto" in pyproject.toml means no @pytest.mark.asyncio needed
async def test_create_user(client: AsyncClient, db: AsyncSession) -> None:
response = await client.post("/api/v1/users", json={
"email": "new@example.com",
"password": "securepass123",
"name": "New User",
})
assert response.status_code == 201
data = response.json()
assert data["email"] == "new@example.com"
assert "password" not in data # PII not leaked
Mocking Patterns
from unittest.mock import AsyncMock, patch, MagicMock
# Patch at the usage site, not the definition site
async def test_sends_email_on_signup(client: AsyncClient) -> None:
with patch("app.services.email.send_welcome_email", new_callable=AsyncMock) as mock_email:
response = await client.post("/api/v1/users", json={...})
assert response.status_code == 201
mock_email.assert_called_once_with(email="new@example.com")
# Use respx for HTTP mocking in async context
import respx
import httpx
@respx.mock
async def test_external_api_call() -> None:
respx.get("https://api.example.com/data").mock(
return_value=httpx.Response(200, json={"value": 42})
)
result = await fetch_external_data()
assert result == 42
Property-Based Tests
from hypothesis import given, strategies as st
@given(
email=st.emails(),
name=st.text(min_size=1, max_size=100),
)
def test_user_creation_never_raises_for_valid_input(email: str, name: str) -> None:
dummy_password = "x" * 12
user = UserCreate(email=email, name=name, password=dummy_password)
assert user.email == email