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

5.0 KiB

Test Writing Rules

Mandatory rules when generating test code. Never violate without explicit user approval.

File Structure

# 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.

# 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:

with pytest.raises(ValueError, match="invalid email"):
    service.create_user(email="bad")

Never assert True or False directly from a function that returns bool:

# 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:

@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:

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:

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:

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