Python Testing
A collection of test-writing best practices for pytest. Designed for AI agents and LLMs to write readable, maintainable, and trustworthy tests.
Categories
Mocking [CRITICAL]
Mock external boundaries only, and keep mocks faithful to real interfaces.
| Rule | Description |
|---|---|
| mock-boundaries | Mock external boundaries, not internal logic |
| mock-autospec | Use autospec to keep mocks faithful to real signatures |
| mock-monkeypatch | Use monkeypatch for env vars and attributes |
Test Structure [HIGH]
Structure tests so failures are easy to diagnose.
| Rule | Description |
|---|---|
| struct-aaa-pattern | Structure tests as Arrange-Act-Assert |
| struct-test-naming | Name tests after the behavior they verify |
| struct-one-behavior | Test one behavior per test |
| struct-no-logic | Keep conditionals and loops out of tests |
Fixtures [HIGH]
Share setup without hiding it.
| Rule | Description |
|---|---|
| fixture-scope | Use the narrowest fixture scope that works |
| fixture-conftest | Place fixtures in the nearest conftest.py |
| fixture-factory | Use factory fixtures for varied test data |
Parametrization [MEDIUM]
Eliminate duplicated tests without hiding cases.
| Rule | Description |
|---|---|
| param-parametrize | Replace duplicated tests with parametrize |
| param-ids | Give parametrized cases readable ids |
Quick Reference
Structure Patterns
def test_expired_token_is_rejected() -> None:
# Arrange
token = make_token(expires_at=YESTERDAY)
# Act
result = validate(token)
# Assert
assert result.valid is False
Fixture Patterns
# Narrow scope by default
@pytest.fixture
def user() -> User:
return User(name="alice", email="alice@example.com")
# Factory fixture for varied data
@pytest.fixture
def make_user() -> Callable[..., User]:
def _make(name: str = "alice", active: bool = True) -> User:
return User(name=name, email=f"{name}@example.com", active=active)
return _make
Parametrize Patterns
@pytest.mark.parametrize(
("email", "valid"),
[
pytest.param("a@example.com", True, id="simple-address"),
pytest.param("a@sub.example.com", True, id="subdomain"),
pytest.param("no-at-sign", False, id="missing-at"),
pytest.param("", False, id="empty-string"),
],
)
def test_email_validation(email: str, valid: bool) -> None:
assert is_valid_email(email) is valid
Mocking Patterns
# Mock the external boundary with a faithful interface.
# `mocker` requires the pytest-mock plugin; with stdlib only,
# use unittest.mock.patch as a decorator or context manager.
def test_notifies_on_signup(mocker: MockerFixture) -> None:
send = mocker.patch("app.signup.email_client.send", autospec=True)
signup(email="alice@example.com")
send.assert_called_once_with(to="alice@example.com", template="welcome")
# monkeypatch for environment variables
def test_reads_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("API_KEY", "test-key")
assert load_config().api_key == "test-key"
See Also
- tooling / test-pytest - pytest configuration, fixtures setup, and coverage