Ejercicio 1 — Auditoría
- Durations típicas de un proyecto Django al llegar aquí: la E2E de compra (2.3 s, legítima pero NO unitaria),
test_expira_reservasconsleep(1)(test del tiempo real),test_envia_emailque arranca SMTP fake (integración disfrazada), y las unitarias honestas en 2-20 ms. La clasificación decide el destino: unitaria → tests/unit con fakes; lenta legítima → marcada@pytest.mark.slowy excluida del pre-commit. - Hallazgos modelo:
test_02_expirarojo si corre solo (dependencia del 01);assert "mientras completabas la compra" in str(...)(literal);time.sleep(1)tras lanzar la tarea (tiempo real);patch("reservations.models.Reservation.objects")(mock de ORM con fake disponible). Cada hallazgo con su fix en la lección. - La migración:
setUp→ fixture (el boilerplate de 12 líneas a 3), y el parametrize reemplaza 3 tests casi-idénticos. Ambos runners en verde: la suite no es del runner, es del proyecto.
Ejercicio 2 — Parametrización
@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- Los refactors (renombrar + extraer
_descuento_volumen) NO matan ninguno: el test habla de entradas y salidas, no de funciones internas. Si murió alguno: probabas el CÓMO (el nombre o la firma interna) — la lección se aplica a ti mismo. 3. Losid=legibles en el reporte:test_precio_por[org-10-pct]en vez detest_precio_por[3]— el reporte es documentación.
Ejercicio 3 — Fakes
- El fixture configurable:
@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" # assertion NUEVA: el estado del mundoEl mock de interacción solo decía "cobrar fue llamado"; el fake permite assertion sobre el ESTADO resultante (saga compensada, asiento libre) — probar efectos, no llamadas. ~30% menos líneas y assertions más fuertes.
- El test con repositorio en memoria: 3-8 ms (vs 120-400 ms del mismo test con BD+transacciones). La suite entera de unitarias en <10 s es la diferencia entre "corro los tests a cada guardado" y "corro los tests antes de pushear".
- El único mock legítimo:
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)Aquí la INTERACCIÓN es el contrato (encolar al commit, la 29): el mock verifica exactamente lo observable desde fuera del proceso — no "cómo lo hizo", sino "qué dijo al mundo".
Ejercicio 4 — Los cazados
- La evidencia:
test_malo_con_patch (tras refactor a select_for_update) → FAILED: filter not called
test_reservar_asiento_ocupado_lanza_conflict → PASSED (1.2s suite completa en verde)El test del CÓMO muere con un refactor legítimo y de rendimiento (¡el select_for_update ES la mejora de la 10!): haberlo tenido en la suite habría bloqueado la optimización o forzado su edición sin cambio de comportamiento — el costo exacto que busca eliminar esta lección.
- Del literal al código:
with pytest.raises(DomainError, match="mientras")queda descartado también (el texto es para humanos); la assertion eserr.code == "seat-unavailable"— el texto puede reescribirse (26) sin tocar un test, porque el CONTRATO es el type del problem+json, no la prosa.
Ejercicio 5 — La convención
# conftest.py
@pytest.fixture
def clock_fijo() -> FakeClock:
"""Reloj virtual a 2027-03-01 12:00 UTC: el tiempo es inyectado, nunca dormido."""
@pytest.fixture
def evento(db) -> Event:
"""Evento futuro con capacidad 100 y precios de la tarifa base."""
return baker.make("events.Event", starts_at=..., capacity=100)
@pytest.fixture
def comprador(db) -> User:
"""Usuario buyer con perfil completo (sin permisos de staff)."""
@pytest.fixture
def gateway() -> FakeGateway:
"""Pasarela fake que aprueba importes < 1000; configurable por test."""
@pytest.fixture
def mailbox() -> list:
"""Bandeja fake de SMTP: assertions sobre emails enviados."""pytest -k reservarencuentra los 5: los nombres son el índice de la suite. 3. La cobertura de servicios con la línea pelada: casi siempre es elexceptde un flujo de error (sin test: añádelo) o código muerto de un feature flag retirado (borra el código, no lo cubras). La cobertura mide lo que NO está probado; no prueba lo que está — el 100% con assertions débiles es vanity, el 90% con assertions de comportamiento es red.
Resumen del profesor
- Unitaria = rápida, determinista, de comportamiento observable; el nombre documenta el contrato (
test_<sujeto>_<escenario>_<resultado>). - Fake antes que mock: el fake prueba estados resultantes, el mock solo llamadas — y solo la interacción que ES contrato.
- La suite sobrevive al refactor cuando sus assertions hablan de entradas/salidas y códigos, no de funciones internas ni prosa.