--- name: pytest-scaffolding description: "Scaffold a maintainable, hierarchical pytest suite for core functionality first, then extend safely. Use when setting up tests, organizing fixtures by dependency, mirroring src structure in tests, or enforcing fast-by-default test runs." argument-hint: "Target scope (for example: app/services/job, app/ai, or full repo)" --- # Pytest Scaffolding Create test scaffolding that is: - Hierarchical: test layout roughly mirrors source layout. - Fast by default: most tests run in under a second total for core units. - Dependency-aware: slow/external dependencies are isolated behind markers and fixture scope. - Extensible: minimal initial skeleton supports adding detailed tests later without refactors. This repository currently uses: - `uv run pytest` as the canonical test invocation. - `pyproject.toml` pytest config under `[tool.pytest.ini_options]`. - strict marker checking (`--strict-markers`). Load [pytest references](./references/pytest-docs.md) when you need detailed rules. ## When To Use - Bootstrapping tests for a new or existing Python repo. - Reorganizing tests that have become flat, slow, or difficult to extend. - Defining fixture boundaries before writing many assertions. - Creating only the first-layer scaffold for core behavior (not exhaustive coverage yet). ## Inputs To Collect 1. Target test scope: full repo, package, or module. 2. Dependency profile: pure Python, DB, network/API, filesystem, UI/browser. 3. Runtime expectation: what must be instant vs allowed to be slower. 4. CI policy: which marker groups must block merges. If these are missing, ask concise clarifying questions before editing. ## Workflow 1. Map source tree to test tree. 2. Classify tests by dependency cost. 3. Create minimal directories and placeholder test modules. 4. Create fixture layers (`tests/conftest.py` plus local `conftest.py` in subtrees only when needed). 5. Register markers and default selection behavior. 6. Run collection and fast path tests. 7. Report gaps and next extension points. ## Step 1: Map Source To Tests Create a mirrored structure rooted at `tests/` that follows major source concepts. Example mapping pattern: - `src/app/services/job.py` -> `tests/app/services/test_job.py` - `src/app/ai/graphs/transcription.py` -> `tests/app/ai/graphs/test_transcription.py` - `src/app/api/routes.py` -> `tests/app/api/test_routes.py` Rules: - One initial test module per core source module. - Prefer `test_.py` naming. - Keep directory mirrors shallow first; add deeper modules only where behavior is complex. ## Step 2: Classify By Dependency Cost Assign each test module to one initial class: - `unit`: no DB/network/filesystem side effects; instant execution. - `integration`: touches DB, HTTP stack, workflow runtime, or external services. - `smoke`: thin end-to-end confidence checks. Decision logic: - If logic can run with fakes/stubs, make it `unit`. - If contract with framework/DB is essential, make it `integration`. - If validating a user-critical path across layers, make it `smoke`. ## Step 3: Scaffold Minimal Test Modules For each target module, scaffold: - import section - one happy-path test function - one error/edge test function - TODO comments indicating detail expansion points Keep assertions minimal but behavior-focused. Avoid large fixtures in module files. ## Step 4: Fixture Layering Strategy Use fixture scopes based on cost: - `function` scope by default. - broader scopes (`module`/`session`) only for expensive setup with clear teardown. Layer fixtures by directory: - `tests/conftest.py`: global, lightweight fixtures only (factories, deterministic defaults). - subtree `conftest.py`: domain-specific fixtures (API client, DB session, AI runtime stubs). Guidelines: - Prefer yield fixtures for setup/teardown. - Keep fixtures atomic (one state-changing responsibility per fixture). - Avoid autouse except for truly universal behavior. ## Step 5: Marker Taxonomy And Config Ensure marker names are explicit and registered in `pyproject.toml` because strict markers are enabled. Recommended baseline markers: - `unit` - `integration` - `smoke` - `slow` - `external` (requires network/service credentials) Default run strategy: - Fast local path: run only `unit` by default in day-to-day iteration. - Full validation path: run all markers in CI or pre-release checks. ## Step 6: Execution And Verification Run commands in this order: 1. `uv run pytest --collect-only -q` 2. `uv run pytest -m unit -q` 3. `uv run pytest -q` (if dependencies are available) Optional targeted runs: - by node id for one test - by `-k` expression for focused iteration ## Step 7: Completion Checks A scaffold pass is complete when all are true: 1. Every core source area has at least one corresponding test module. 2. Unit tests run quickly and deterministically. 3. Integration/external tests are isolated by marker and fixture boundaries. 4. No unregistered marker warnings/errors. 5. `tests/` structure is understandable without extra documentation. 6. A clear TODO path exists for deepening assertions later. ## Branching Scenarios - If external APIs are required: provide stubs/mocks for unit tests; guard real calls behind `external` marker. - If DB is required: build a dedicated integration fixture layer and keep unit tests DB-free. - If tests become slow: split slow tests via marker and widen fixture scope only where safe. - If naming conflicts appear: keep unique test module names or package test directories explicitly. ## Output Format When applying this skill, provide: 1. Proposed test tree diff. 2. Marker and fixture plan. 3. Exact commands for fast path and full path. 4. Risks/open questions before writing detailed assertions.