118 lines
5.0 KiB
Markdown
118 lines
5.0 KiB
Markdown
---
|
|
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.
|