# Python Testing

> When to activate: pytest, TDD, test fixtures, parametrize, async tests, coverage, mocking, hypothesis

- Skill: `mattakushi432/python-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/python-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/python-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/python-testing

---


# Python Testing Patterns

## pytest Setup
```toml
# 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
```python
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
```python
@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
```python
# 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
```python
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
```python
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
```

