2.5 KiB
2.5 KiB
Test Guard — Python / pytest Patterns
Concrete applications of the nine rules for pytest projects. Read this when reviewing or writing Python tests.
Rule 2: Mock boundaries in Python
Justified mock targets:
- HTTP clients:
httpx,requests,aiohttp(or userespx/responsesinstead of raw mocks) - LLM SDK calls:
openai,anthropic,litellm.completionand friends - Database sessions, when the database is not the subject (see Rule 9)
- Filesystem I/O on external paths (
tmp_pathfixture is often better than mocking) - Clock and randomness:
time.time,datetime.now,random(preferfreezegunor injected clocks)
Unjustified mocks (common agent-generated violations):
MagicMock()standing in for a Pydantic model or dataclass — construct the real thing- Mocking internal utility functions to isolate a "unit"
- Mocking
json.loads/json.dumpsor other stdlib pure functions
Rule 3: Parametrize
# Violation: three copy-pasted tests differing by one value
def test_slug_lowercase(): ...
def test_slug_strips_spaces(): ...
def test_slug_handles_unicode(): ...
# Fix
@pytest.mark.parametrize(
("raw", "expected"),
[
("Hello World", "hello-world"),
(" padded ", "padded"),
("Café Menu", "cafe-menu"),
],
)
def test_slugify_normalizes_input(raw, expected):
assert slugify(raw) == expected
Rule 8: Real Pydantic/dataclass instances
# Wrong — hides field typos and validation errors
state = MagicMock()
state.user_id = "123"
state.status = "ACTIVE"
# Right — Pydantic validates the construction itself
state = UserState(user_id="123", status="ACTIVE")
If a model needs many fields, add a factory fixture or use factory_boy — don't fall back to MagicMock.
Rule 9: Real database via fixtures
Use a fixture that applies real migrations (e.g., a session-scoped test database with alembic upgrade head), and function-scoped transactions rolled back per test. pytest-postgresql, testcontainers, or an SQLite-compatible fallback all work; the point is real schema, not a mocked session, whenever query or persistence logic is the subject.
pytest-specific smells
assert mock.call_count == Non anything internal — Rule 1 violation@patchstacks three or more deep — the test is coupled to implementation; restructure- Asserting log output via
caplogfor messages no caller parses — Rule 4 violation - Fixtures that build mocks of project classes — Rule 8 violation, make the fixture build real objects