Unit Tests in Python
A unit test checks one useful behavior in a controlled context. “Unit” is a design boundary, not necessarily one function: test the smallest surface that can express the contract without recreating its implementation in the test.
import pytest
@pytest.mark.parametrize(
("raw", "expected"),
[(" Ada ", "ada"), ("GRACE", "grace")],
)
def test_normalize_name(raw: str, expected: str) -> None:
assert normalize_name(raw) == expected
def test_normalize_name_rejects_empty_text() -> None:
with pytest.raises(ValueError, match="empty"):
normalize_name(" ")
Python includes unittest; pytest is a third-party runner with concise
assertions, fixtures, and parametrization. Both are viable. Prefer one consistent
project convention over mixing styles without a reason.
Shape of a useful test
- Arrange only the state relevant to the scenario.
- Act through a public or intentionally stable boundary.
- Assert the observable result, state change, emitted event, or failure.
- Give the test a name that identifies the behavior and condition.
Parametrization is valuable when many inputs share one contract. Separate cases when setup, expected behavior, or failure diagnosis differs. Parameter values may be mutable and are not automatically copied between pytest cases.
Fixtures and isolation
Fixtures own setup and cleanup for reusable resources. Keep their scope as narrow
as practical; shared mutable session state couples tests and makes failures
order-dependent. Use temporary-directory support such as pytest's tmp_path
instead of writing into the repository or a user's real configuration.
A test should control time, randomness, environment variables, and external I/O when those would otherwise make the result nondeterministic. Seeded randomness is reproducible, but it covers only the generated examples unless the seed set is deliberately varied.
Test doubles
Fakes, stubs, spies, and mocks replace collaborators for different reasons. Patch where a name is looked up, not where its original definition happens to live. Prefer a small fake implementation or injected callable when it makes the contract clearer.
Over-mocking creates tests that merely confirm a private call sequence. Keep a real integration test for important boundaries such as databases, HTTP clients, filesystems, serialization, and subprocesses.
Durable assertions
Assert semantic facts, not incidental ordering, formatting, timestamps, or full object snapshots unless those are public contracts. Check both results and important side effects. For failures, assert the exception type and meaningful details without pinning an unstable entire message.
Tests must be able to fail for the intended regression. When repairing a bug, observe the new test fail against the broken behavior before accepting the fix when doing so is practical.