pytest is the de-facto standard (unittest is in the stdlib, JUnit-style, but rarely chosen for new code).
From now on, every exercise comes with pytest tests instead of asserts in main().
class CalculatorTest {
@Test
void addsNumbers() {
assertEquals(4, Calculator.add(2, 2));
}
}def test_adds_numbers():
assert add(2, 2) == 4- Test files are named
test_*.py, and test functionstest_*. No class needed (you can group them inclass TestX:). - A plain
assert: pytest rewrites it to show both sides on failure. NoassertEqualszoo. - Run with
uv run pytest,uv run pytest path/to/test_file.py::test_name, or-k "slug"(name filter). Use-xto stop at the first failure and-vfor verbose output.
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError, match="division"): # ≈ assertThrows
divide(1, 0)
def test_float():
assert 0.1 + 0.2 == pytest.approx(0.3)@pytest.fixture
def store(): # setup
s = NoteStore()
s.add("RAG")
yield s # the test runs here
s.close() # teardown (optional)
def test_len(store): # request a fixture by naming a parameter
assert len(store) == 1- Each test gets a fresh fixture (
scope="function"by default). Usescope="module"/"session"for expensive ones. - Fixtures can depend on other fixtures.
- Shared fixtures go in
conftest.py, which pytest discovers automatically. No imports needed. - Built-in fixtures:
tmp_path(a fresh tempPath),monkeypatch,capsys(captured stdout),caplog.
@pytest.mark.parametrize("text, expected", [
("Hello World", "hello-world"),
(" spaces ", "spaces"),
("C++ & Java!", "c-java"),
])
def test_slugify(text, expected):
assert slugify(text) == expectedfrom unittest.mock import Mock
def test_summary_uses_client():
client = Mock()
client.summarize.return_value = "short" # when(client.summarize(any())).thenReturn("short")
assert summarize_note(note, client) == "short"
client.summarize.assert_called_once_with(note.body) # verify(client).summarize(note.body)
client.fetch.side_effect = TimeoutError() # thenThrowBecause of duck typing, you often don't need a mocking library at all. Pass a tiny fake class. Prefer fakes (a real in-memory implementation) over mocks for repositories.
monkeypatch swaps attributes and environment variables for one test and undoes it afterwards:
def test_reads_api_key(monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
monkeypatch.setattr(module, "now", lambda: FIXED_TIME)Inject dependencies (a repository, a clock, an HTTP client) through the constructor or as function
parameters, exactly as you'd do with Spring, minus the container. That's what makes NoteService in the exercise testable.
uv run pytest lessons/08_testing/demo -vexercise/note_service.py is finished code with one bug in it. Your job:
- Write the tests in
exercise/test_note_service.py. Each TODO test currently fails withpytest.fail("TODO"). - Use fixtures,
parametrize,pytest.raises, a fake clock, and aMock. - One of your tests should expose the bug. Then fix it in
note_service.py.
uv run pytest lessons/08_testing/exercise -vDone = all green, with the bug fixed.