Writing Effective Python Tests
Test Structure
Mirror the module layout
Keep a one-to-one relationship between test files and the modules they cover: myapp/service.py → tests/.../test_service.py. This makes the test for any given module obvious and keeps coverage gaps visible.
Follow AAA (Arrange, Act, Assert)
Structure each test body in three beats — set up inputs (Arrange), call the thing under test (Act), then assert on the result. Keep them in that order; don't interleave more setup after the act.
Atomic unit tests
Each test should verify a single behavior. The test name should tell you what's broken when it fails. Multiple assertions are fine when they all verify the same behavior.
python# Good: Name tells you what's broken def test_user_creation_sets_defaults(): user = User(name="Alice") assert user.role == "member" assert user.id is not None assert user.created_at is not None # Bad: If this fails, what behavior is broken? def test_user(): user = User(name="Alice") assert user.role == "member" user.promote() assert user.role == "admin" assert user.can_delete_others()
Use parameterization for variations of the same concept
pythonimport pytest @pytest.mark.parametrize("input,expected", [ ("hello", "HELLO"), ("World", "WORLD"), ("", ""), ("123", "123"), ]) def test_uppercase_conversion(input, expected): assert input.upper() == expected
Don't parameterize unrelated behaviors — if the test logic differs, write separate tests.
Project-Specific Rules
Imports at module level
Put ALL imports at the top of the file. Do not import inside test function bodies.
python# Correct import pytest from myapp.service import do_work def test_something(): assert do_work() is not None # Wrong - no local imports def test_something(): from myapp.service import do_work # Don't do this ...
Async tests
If the project sets asyncio_mode = "auto" in pyproject.toml, write async tests without decorators:
python# Correct (when asyncio_mode = "auto") async def test_async_operation(): result = await some_async_function() assert result == expected
Otherwise, mark explicitly with @pytest.mark.asyncio.
Inline snapshots for complex data
If the project uses inline-snapshot, use it for JSON schemas and complex structures:
pythonfrom inline_snapshot import snapshot def test_schema_generation(): schema = generate_schema(MyModel) assert schema == snapshot() # Will auto-populate on first run
Commands:
pytest --inline-snapshot=create- populate empty snapshotspytest --inline-snapshot=fix- update after intentional changes
Fixtures
Share setup through fixtures, not setup/teardown methods
Put shared fixtures in conftest.py so they're available across test files without imports. Use fixtures (and their yield-based teardown) for setup and cleanup — avoid xUnit-style setUp/tearDown methods.
Prefer function-scoped fixtures
python@pytest.fixture def client(): return Client() def test_with_client(client): result = client.ping() assert result is not None
Use tmp_path for file operations
pythondef test_file_writing(tmp_path): file = tmp_path / "test.txt" file.write_text("content") assert file.read_text() == "content"
Mocking
Mock at the boundary
Use pytest-mock's mocker fixture (preferred) or unittest.mock patches.
pythonfrom unittest.mock import AsyncMock async def test_external_api_call(mocker): mock = mocker.patch("mymodule.external_client.fetch", new_callable=AsyncMock) mock.return_value = {"data": "test"} result = await my_function() assert result == {"data": "test"}
Don't mock what you own
Test your code with real implementations when possible. Mock external services (HTTP APIs, third-party SDKs), not your own internal classes.
Don't write unit tests against infrastructure components
Orchestrators, model-serving runtimes, observability clients, and similar infrastructure should be exercised via integration tests, not unit tests with mocks of their internals.
Test Naming
Test files must be named test_*.py and test functions test_* so pytest discovers them. Beyond that, use descriptive names that explain the scenario:
python# Good def test_login_fails_with_invalid_password(): def test_user_can_update_own_profile(): def test_admin_can_delete_any_user(): # Bad def test_login(): def test_update(): def test_delete():
Running Tests
Prefer project Make targets when available: make unit-tests, make integration-tests, make tests. Otherwise uv run pytest -n auto (parallel). The suite must finish with 0 warnings.


