pytest async-first testing best practices for Python backends — covers test layer structure (unit / integration / e2e mirroring `app/`), fixture design with proper scope and yield-based teardown, async testing with `pytest-asyncio` (and `asyncio_mode = "auto"` config), test-data generation with `factory_boy` + `faker`, HTTP mocking with `respx` for `httpx` clients, time mocking with `freezegun`, property-based testing with `hypothesis`, parametrize patterns with `ids=` and `pytest.param`, database test isolation via transaction rollback, the `flush()` pattern for chained DB fixtures under parallel `pytest-xdist` execution, coverage configuration, and CI-friendly defaults. Use this skill whenever the user is writing, reviewing, debugging, or designing pytest tests — including any work involving `@pytest.fixture`, `@pytest.mark.asyncio`, `@pytest.mark.parametrize`, `conftest.py`, async fixtures, factory_boy factories, `respx` mocks, `freezegun`, `hypothesis` strategies, `pytest-xdist` parallelism, `pytest-cov` coverage, or test isolation patterns. Trigger even when the user just shows code under `tests/` or imports from `pytest`, `pytest_asyncio`, `factory`, `faker`, `respx`, `freezegun`, or `hypothesis`. Do NOT use for `unittest` stdlib tests, `nose`, `doctest`, or generic Python questions unrelated to testing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pytest-best-practices:pytest-best-practicesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill captures production-tested defaults for pytest with async support, factory-driven test data, real-DB integration testing, and CI-friendly configuration. It assumes the test target is a Python backend (FastAPI, SQLAlchemy, async services). UI / Selenium / Playwright e2e patterns are out of scope.
This skill captures production-tested defaults for pytest with async support, factory-driven test data, real-DB integration testing, and CI-friendly configuration. It assumes the test target is a Python backend (FastAPI, SQLAlchemy, async services). UI / Selenium / Playwright e2e patterns are out of scope.
For HTTP-side test client patterns in FastAPI apps, defer to the fastapi-best-practices skill (rule 29 covers httpx.AsyncClient setup with ASGITransport). For DB fixture details specific to async SQLAlchemy 2.0 sessions and expire_on_commit=False semantics, cross-reference the sqlalchemy-best-practices skill — this one stays focused on pytest mechanics.
The rules below assume:
pytest + pytest-asyncio for the async event loopfactory_boy + faker for test data generationpytest-mock for the mocker fixture (cleaner than raw unittest.mock)respx for httpx HTTP mockingfreezegun for time mockinghypothesis for property-based testingpytest-xdist for parallel runspytest-cov for coveragepytest-benchmark for performance regressionsMirror the app structure under tests/:
app/services/payment.py → tests/unit/services/test_payment.py
app/api/users.py → tests/integration/api/test_users.py
Three layers, progressively wider scope:
tests/unit/ — fast, isolated, mock everything external. Target <1s per file.tests/integration/ — real database (testcontainers / ephemeral PG), real ORM, mocked external HTTP.tests/e2e/ — full stack including external services (or recorded fixtures via respx).Run them in CI in order: unit first (fail fast), then integration, then e2e. Use -m unit markers for gating.
Pick fixture scope deliberately: function (default, fresh per test), module (shared across file), session (one per test run).
session-scoped fixtures with mutable state leak between tests; function-scoped fixtures for expensive resources (DB engines, HTTP clients) make the suite slow. Default to function; promote to module/session only for genuinely immutable / setup-heavy resources.Always clean up — yield-based fixtures with explicit teardown, or DB transaction rollback (see #5). Never rely on test order to cleanup.
Use pytest-mock's mocker fixture over raw unittest.mock.patch. Auto-cleanup at test end, less boilerplate, plays well with async.
Mock outbound HTTP with respx, not generic mocks:
async def test_fetch_user(respx_mock):
respx_mock.get("https://api.example.com/users/42").mock(
return_value=Response(200, json={"id": 42, "name": "Alice"})
)
result = await fetch_user(42)
assert result.name == "Alice"
Database tests: wrap each test in a transaction that rolls back at teardown. Faster than TRUNCATE-per-test, gives full isolation by construction.
Mark async tests with @pytest.mark.asyncio. Configure once globally so you don't decorate every test:
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
With auto mode, every async def test_... is treated as pytest.mark.asyncio automatically.
Async fixtures use the same machinery — @pytest_asyncio.fixture (or just @pytest.fixture under asyncio_mode = "auto"). Use async with inside the fixture for resource lifecycle.
Never hardcode entity IDs in tests. Let the DB generate them.
pytest-xdist parallel execution; tests passing solo flake under -n auto. Even sequential tests break when test order changes.Use the flush pattern to get the DB-assigned ID without committing the transaction:
@pytest_asyncio.fixture
async def user(db_session):
user = User(email="[email protected]", name="Test User")
db_session.add(user)
await db_session.flush() # populates user.id without commit
yield user
# transaction rollback at session teardown handles cleanup
@pytest_asyncio.fixture
async def user_profile(db_session, user):
profile = UserProfile(user_id=user.id, bio="Test bio")
db_session.add(profile)
await db_session.flush()
yield profile
flush() sends INSERT without committing, so the auto-generated PK is available to dependent fixtures while the entire fixture tree still rolls back at the end of the test.Chain fixtures explicitly via parameters — parent fixture yields the entity, child fixture takes it as a dependency. pytest resolves the graph; you don't manage order manually.
Use @pytest.mark.parametrize for table-driven tests instead of N near-duplicate test functions:
@pytest.mark.parametrize("input,expected", [
("", ValidationError),
("invalid", ValidationError),
("[email protected]", None),
])
def test_email_validation(input, expected):
if expected:
with pytest.raises(expected):
validate_email(input)
else:
validate_email(input) # should not raise
For complex parameter sets, name them via ids= or use pytest.param(..., id="...") for readable failure output.
pytest.param(..., marks=pytest.mark.xfail(reason="...")) documents known-broken cases without removing them — they re-pass loudly when fixed.
Mock at boundaries, not internals. Boundaries = network, file I/O, time, randomness, external services. Internals = your own functions, classes, repositories.
Time-dependent code: use freezegun:
@freeze_time("2026-01-15 12:00:00")
def test_password_reset_token_expires_in_24h():
token = generate_reset_token()
# ...
datetime.now() makes tests flaky at midnight or DST transitions. Freeze the clock for deterministic time math. Caveat: freezegun doesn't intercept time.monotonic() — use it for wall-clock, not benchmark timing.Property-based testing with hypothesis for invariant-heavy logic (parsers, serializers, math, state machines):
@given(st.integers(min_value=0), st.integers(min_value=0))
def test_addition_commutative(a, b):
assert add(a, b) == add(b, a)
Snapshot testing (syrupy, pytest-snapshot) for stable-output transformations — diff against committed snapshots, regenerate explicitly when intentional changes happen.
Performance regressions: pytest-benchmark runs a function repeatedly and asserts mean/median against a stored baseline. Useful for hot paths; running in normal CI is overhead.
--cov-branch so if/else paths both count.pytest --cov=app --cov-report=term-missing --cov-branch. Fail CI on coverage drop with --cov-fail-under=80.pytest-xdist: pytest -n auto runs across CPU cores. Requires test isolation (no shared mutable state, no static IDs) — see #1, #5, #8.asserts are fine when verifying the same behavior from different angles ("created user has correct email AND is active"); split when they verify different behaviors.test_usertest_calculate_discount_with_zero_percentage_returns_original_pricerandom.random() (use hypothesis or seeded RNG), no real network, no datetime.now() (use freezegun).-m unit markers and run incrementally.factory_boy factories accept overrides, scale better than dicts of mock data, and integrate with faker for realistic values.conftest.py with db_session / client fixtures, conform to them rather than introducing parallel patterns. Add to the convention; don't fragment it.fastapi-best-practices (rule 29 — httpx.AsyncClient with ASGITransport). For SQLAlchemy session / ORM-specific test fixtures, defer to sqlalchemy-best-practices.npx claudepluginhub mvolkov83/skills --plugin pytest-best-practicesProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.