--- name: pytest-scaffolding description: "Scaffold a maintainable, hierarchical pytest suite with fast defaults and clear escalation paths for FastAPI and SQLAlchemy tests. Use when creating or reorganizing tests, defining fixture/marker boundaries, or making test strategy progressively discoverable." argument-hint: "Target scope plus stack details (pure Python, FastAPI, SQLAlchemy sync, SQLAlchemy async, or mixed)" --- # Pytest Scaffolding Create test scaffolding that stays fast for daily work and scales safely as dependencies increase. This skill is optimized for progressive discoverability: 1. Start with the shortest path in this file. 2. Load exactly one deeper reference only when a decision requires it. 3. Continue only as far as needed for the current task. Repository defaults: - `uv run pytest` is the canonical invocation. - pytest settings live in `pyproject.toml` under `[tool.pytest.ini_options]`. - strict marker checking is expected (`--strict-markers`). ## Discovery Ladder ### Level 0: Scope And Stack Triage (always) Collect: 1. Target scope (repo, package, module). 2. Stack shape (pure Python, FastAPI, SQLAlchemy sync, SQLAlchemy async, or mixed). 3. Speed target (what must stay instant). 4. CI gate policy (which marker groups block merge). If any are missing, ask concise clarifying questions before scaffolding. ### Level 1: Core pytest scaffold (default) Use this for all stacks first: 1. Mirror `src/` into `tests/` with one starter file per core module. 2. Classify test intent by cost: - `unit`: no DB/network/filesystem side effects. - `integration`: framework, DB, or multi-layer contracts. - `smoke`: thin critical-path checks. 3. Scaffold each new module with: - one happy-path test, - one failure/edge test, - TODO anchors for deeper assertions. 4. Keep fixtures layered: - global lightweight fixtures in `tests/conftest.py`, - domain fixtures in subtree `conftest.py` only when needed. 5. Register markers early: `unit`, `integration`, `smoke`, `slow`, `external`. 6. Validate in order: - `uv run pytest --collect-only -q` - `uv run pytest -m unit -q` - `uv run pytest -q` when dependencies are available. Load next reference only if needed: - Baseline details and rationale: [pytest-docs.md](./references/pytest-docs.md) - Condensed quick path variant: [pytest-docs copy.md](./references/pytest-docs copy.md) ### Level 2: FastAPI branch (only for HTTP/dependency/lifespan concerns) Escalate here when testing API routes, dependency injection boundaries, or app lifespan behavior. Apply these defaults: 1. Prefer `TestClient` with sync `def` tests for route behavior. 2. Use `AsyncClient` + `@pytest.mark.anyio` only when test logic must await other async work. 3. Prefer `app.dependency_overrides` over patching internals. 4. Reset dependency overrides in teardown after every test/fixture. 5. For startup/shutdown semantics: - use `TestClient` as context manager, or - use `LifespanManager` with async client. Marker intent in FastAPI-heavy suites: - `unit`: service logic without HTTP/DB. - `integration`: route + DI + DB contract checks. - `smoke`: one request per critical user path. Reference: [fastapi-testing.md](./references/fastapi-testing.md) ### Level 3: SQLAlchemy branch (only for DB transaction/session design) Escalate here when session lifecycle, transaction isolation, or async ORM behavior matters. Apply these defaults: 1. Create engine once per test session. 2. Open connection + outer transaction per test. 3. Bind session with `join_transaction_mode="create_savepoint"`. 4. Allow code under test to call `commit()` safely; rollback outer transaction at test end. 5. Keep unit tests DB-free; DB tests belong under `integration`. Async additions: - use async fixtures and `@pytest.mark.anyio`. - set `expire_on_commit=False` for `AsyncSession`. - avoid implicit lazy IO; use eager loading (`selectinload`) or explicit refresh. SQLite in-memory with threaded test clients: - use `StaticPool` when required by thread/connection sharing. Reference: [sqlalchemy-testing.md](./references/sqlalchemy-testing.md) ## Branching Logic Summary - If pure logic can be faked cleanly, keep in `unit`. - If framework/DB contract is the behavior under test, use `integration`. - If external service credentials/network is required, gate behind `external`. - If suite slows down, split by marker before broadening fixture scope. - If async relationship access raises `MissingGreenlet`, switch to eager loading strategy. ## Completion Checks A scaffold pass is complete when all are true: 1. Core source areas map to clear test modules. 2. Fast path (`-m unit`) is deterministic and quick. 3. Integration and external paths are isolated by fixtures and markers. 4. No unregistered-marker failures occur. 5. Structure is understandable without extra oral context. 6. Clear TODO extension points exist for deeper assertions. ## Output Contract When this skill is applied, return: 1. Proposed test tree diff. 2. Marker and fixture plan. 3. Exact fast-path and full-path commands. 4. Which reference level was loaded and why. 5. Risks or open questions before expanding assertions.