Files
github-copilot/skills/autotest-writer/references/write-rules.md
T
ВяткинАртём d00f858d98 fix
2026-04-06 17:54:07 +03:00

169 lines
5.0 KiB
Markdown

# 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