Exercise 1 — Audit
- Typical durations of a Django project arriving here: the purchase E2E (2.3 s, legitimate but NOT unit),
test_expira_reservaswithsleep(1)(real-time test),test_envia_emailthat boots a fake SMTP (integration in disguise), and the honest unit tests at 2-20 ms. The classification decides the destination: unit → tests/unit with fakes; slow-legitimate → marked@pytest.mark.slowand excluded from pre-commit. - Model findings:
test_02_expirared when run alone (dependency on 01);assert "mientras completabas la compra" in str(...)(literal);time.sleep(1)after launching the task (real time);patch("reservations.models.Reservation.objects")(ORM mock with a fake available). Each finding with its fix in the lesson. - The migration:
setUp→ fixture (12 lines of boilerplate down to 3), and the parametrize replaces 3 near-identical tests. Both runners green: the suite belongs to the project, not the runner.
Exercise 2 — Parametrization
@pytest.mark.parametrize(
"seats,total",
[pytest.param(1, 5000, id="un-asiento"),
pytest.param(3, 13500, id="tres-descuento-10"),
pytest.param(10, 40000, id="diez-descuento-20"),
pytest.param(0, None, id="cero-lanza", marks=pytest.mark.xfail(raises=DomainError)),
pytest.param(-1, None, id="negativo-lanza", marks=pytest.mark.xfail(raises=DomainError)),
pytest.param(1000, 3_500_000, id="limite-capacidad")],
)
def test_precio_por(seats, total, evento):
assert precio_por(evento, seats) == total- The refactors (rename + extract
_descuento_volumen) kill none: the test speaks of inputs and outputs, not internal functions. If one died: you were testing the HOW (the name or the internal signature) — the lesson applies to yourself. 3. The readableid=s in the report:test_precio_por[org-10-pct]instead oftest_precio_por[3]— the report is documentation.
Exercise 3 — Fakes
- The configurable fixture:
@pytest.fixture
def gateway_rechaza():
gw = FakeGateway()
gw.declina_todo = True
return gw
def test_pago_rechazado_compensa(reserva, gateway_rechaza, clock_fijo):
with pytest.raises(PaymentDeclined):
confirmar_compra(reserva, gateway=gateway_rechaza, clock=clock_fijo)
assert saga.refresh_from_db() or saga.estado == "COMPENSATED" # NEW assertion: the world's stateThe interaction mock only said "cobrar was called"; the fake allows asserting on the resulting STATE (compensated saga, free seat) — testing effects, not calls. ~30% fewer lines and stronger assertions.
- The in-memory repository test: 3-8 ms (vs 120-400 ms for the same test with DB+transactions). The whole unit suite in <10 s is the difference between "I run the tests on every save" and "I run the tests before pushing".
- The one legitimate mock:
def test_reserva_encola_email_al_commit(reserva, mocker):
tarea = mocker.patch("reservations.tasks.enviar_email_confirmacion.delay")
with captureOnCommitCallbacks(execute=True):
reservar(...)
tarea.assert_called_once_with(reserva.public_ref)Here the INTERACTION is the contract (enqueue at commit, 29): the mock verifies exactly what is observable from outside the process — not "how it did it", but "what it said to the world".
Exercise 4 — The hunted
- The evidence:
test_malo_con_patch (after refactoring to select_for_update) → FAILED: filter not called
test_reservar_asiento_ocupado_lanza_conflict → PASSED (whole suite green in 1.2s)The HOW test dies with a legitimate and performant refactor (the select_for_update IS 10's improvement!): having it in the suite would have blocked the optimization or forced editing it with no behavior change — the exact cost this lesson seeks to eliminate.
- From literal to code:
with pytest.raises(DomainError, match="mientras")is also ruled out (the text is for humans); the assertion iserr.code == "seat-unavailable"— the text can be rewritten (26) without touching a test, because the CONTRACT is the problem+json's type, not the prose.
Exercise 5 — The convention
# conftest.py
@pytest.fixture
def clock_fijo() -> FakeClock:
"""Virtual clock at 2027-03-01 12:00 UTC: time is injected, never slept."""
@pytest.fixture
def evento(db) -> Event:
"""Future event with capacity 100 and base-tier prices."""
return baker.make("events.Event", starts_at=..., capacity=100)
@pytest.fixture
def comprador(db) -> User:
"""Buyer user with a full profile (no staff permissions)."""
@pytest.fixture
def gateway() -> FakeGateway:
"""Fake gateway approving amounts < 1000; configurable per test."""
@pytest.fixture
def mailbox() -> list:
"""Fake SMTP inbox: assertions over sent emails."""pytest -k reservarfinds the 5: the names are the suite's index. 3. The services coverage with the bare line: it is almost always an error-flowexcept(no test: add one) or dead code from a retired feature flag (delete the code, don't cover it). Coverage measures what is NOT tested; it doesn't prove what is — 100% with weak assertions is vanity, 90% with behavior assertions is a net.
Professor's summary
- Unit test = fast, deterministic, observable behavior; the name documents the contract (
test_<subject>_<scenario>_<result>). - Fake before mock: the fake tests resulting states, the mock only calls — and only the interaction that IS contract.
- The suite survives refactoring when its assertions speak of inputs/outputs and codes, not internal functions or prose.