# Test Writing Rules Mandatory rules when generating test code. Never violate without explicit user approval. ## File Structure ```python # 1. stdlib imports # 2. third-party imports # 3. local imports (blank line between groups — match project convention) # 4. Module-level constants or type aliases (if needed) # 5. Fixtures local to this file (if not in conftest) # 6. Test functions / class ``` Always match the import grouping style found in existing tests. ## Naming | Target | Convention | Example | |--------|-----------|---------| | Test file | `test_{module}.py` or `{module}_test.py` (match project) | `test_auth.py` | | Test function | `test_{what}_{condition}` | `test_login_wrong_password` | | Test class | `Test{Subject}` | `TestLoginService` | | Fixture | `{noun}` or `{noun}_{qualifier}` | `db_session`, `admin_user` | | Parametrize ID | descriptive string | `"valid"`, `"empty_email"` | ## Type Annotations — Python 3.13+ All test functions must have return type `-> None`. Fixture functions must have explicit return type annotation. ```python # Correct def test_login_valid(db_session: AsyncSession, user: User) -> None: ... @pytest.fixture def admin_user(db_session: AsyncSession) -> User: ... ``` Never use `Optional[X]` — use `X | None`. Never use `List[X]`, `Dict[K, V]` — use `list[X]`, `dict[K, V]`. ## Assertions Use the same assertion style as the project (discovered in Step 2). For plain `assert` style: - Always compare with `==`, not `is`, for value equality - Use `assert x is None` / `assert x is not None` for None checks - Use `assert isinstance(x, SomeClass)` for type checks - Include a failure message for non-obvious assertions: `assert result.status == 200, f"Expected 200, got {result.status}"` For exception assertions: ```python with pytest.raises(ValueError, match="invalid email"): service.create_user(email="bad") ``` Never assert `True` or `False` directly from a function that returns bool: ```python # Bad assert service.is_valid(x) == True # Good assert service.is_valid(x) ``` ## Fixtures - Prefer existing fixtures from conftest.py over creating new ones - Create a new fixture only if it will be reused in ≥ 2 tests in this file - One-off setup → inline in the test body - Scope: use `function` by default; `module`/`session` only for read-only shared resources - Never use `autouse=True` for fixtures defined inside a test file ## Parametrize Use `@pytest.mark.parametrize` when the same logic is tested with ≥ 3 data variants: ```python @pytest.mark.parametrize( ("email", "expected_error"), [ ("", "Email cannot be empty"), ("not-an-email", "Invalid email format"), ("a" * 256 + "@x.com", "Email too long"), ], ) def test_create_user_invalid_email( email: str, expected_error: str, db_session: AsyncSession, ) -> None: with pytest.raises(ValueError, match=expected_error): UserService(db_session).create_user(email=email) ``` ## Mocking Match the project's mock style (discovered in Step 2). With `pytest-mock`: ```python def test_sends_email(mocker: MockerFixture) -> None: send = mocker.patch("myapp.notifications.send_email") service.register(email="u@example.com") send.assert_called_once_with("u@example.com", subject=mocker.ANY) ``` With `unittest.mock`: ```python from unittest.mock import AsyncMock, patch @patch("myapp.notifications.send_email", new_callable=AsyncMock) async def test_sends_email(mock_send: AsyncMock) -> None: await service.register(email="u@example.com") mock_send.assert_awaited_once() ``` Use `create_autospec` when the interface must be enforced: ```python mock_repo = create_autospec(UserRepository) ``` Never use `MagicMock()` without speccing — it silently accepts any attribute. ## Async Tests Match `asyncio_mode` from `pyproject.toml`: - `asyncio_mode = "auto"` → no decorator needed, just `async def test_` - `asyncio_mode = "strict"` → must add `@pytest.mark.asyncio` Never mix sync and async in the same test — use `AsyncMock` for async dependencies. ## Test Isolation Rules - Each test must be independent — no shared mutable state between tests - Never rely on test execution order - Clean up side effects: files, DB rows, environment variables - Use `monkeypatch` for environment variable overrides (not `os.environ` direct) - Use `tmp_path` fixture for temporary file I/O ## Scenario Coverage per Function At minimum, cover: | Scenario | Must have | |----------|-----------| | Happy path | ✅ Always | | Empty / zero input | ✅ If applicable | | Boundary values | ✅ If numeric limits exist | | Invalid type or format | ✅ If input is validated | | Not found / missing resource | ✅ For DB/file lookups | | Permission denied | ✅ If auth is involved | | External service failure | ✅ If network/DB call exists | ## What NOT to Test - Internal private methods (`_method`) directly — test via public API - Framework internals (Django ORM, SQLAlchemy internals) - Third-party library behaviour (mock at the boundary, don't re-test the lib) - Trivial getters/setters with zero logic