Exercise 1 — The operated view
Expected after (the whole view):
@api_view(["POST"])
def reserve_view(request):
serializer = ReserveInputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
reservation = reservar(request.user, **serializer.validated_data)
except DomainError as exc:
return problem(exc) # 13
return ReservationOut(reservation, status=201)~15 lines vs 150: HTTP is translation, the domain lives in the service, and manage.py shell >>> reservar(u,...) works without a request — the cotton test. If it failed: some request/Response is still hiding in the service (remove it: inject data, not HTTP).
Exercise 2 — Clock
class FakeClock:
def __init__(self, fixed: datetime): self._fixed = fixed
def now(self) -> datetime: return self._fixed
def test_reserva_expira_diez_minutos_despues():
clock = FakeClock(datetime(2027, 3, 1, 23, 59, tzinfo=dt.UTC))
r = reservar(user=self.buyer, event_uuid=..., seat_refs=[...], clock=clock)
self.assertEqual(r.expires_at, datetime(2027, 3, 2, 0, 9, tzinfo=dt.UTC))No sleeps or freezegun patches: the dependency enters by parameter and the test is deterministic. Lesson 29's job will use the same Clock for "now" — a single place where time is negotiable in tests.
Exercise 3 — L
- The test that catches it:
def test_gateway_siempre_responde_o_lanza(self):
with self.assertRaises((GatewayError, ChargeResult)):
self.broken_gateway.charge(Amount(50), intent)If the broken implementation returns None, assertRaises fails (nothing was raised) and the type doesn't match: substitutability breaks in tests before production — that is the value of L with Protocols + contract tests per implementation.
- Docstring contract: "Returns ChargeResult(status=SUCCEEDED|PENDING). Raises GatewayError on temporary failure and DomainConflict on business rejection. Guarantees idempotency by intent.id (14)". With strict typing (
-> ChargeResult) mypy catches the None statically: docstring + types = an honest contract.
Exercise 4 — O
PAYMENT_METHODS: dict[str, type[PaymentMethod]] = {}
def register(name: str):
def deco(cls):
PAYMENT_METHODS[name] = cls
return cls
return deco
@register("gift_card")
class GiftCardMethod(PaymentMethod): ...
@register("cash_on_delivery")
class CodMethod(PaymentMethod): ...Adding "crypto" = 1 new file with its class + registration; the service iterates PAYMENT_METHODS[name] without knowing implementations. The if/elif would have touched N places (an O violation and a merge risk). Fine note: the explicit registry avoids the magic auto-imports that confuse people.
Exercise 5 — I
- First attempt: a "ReservationRepository" interface with 14 methods. The public endpoint uses 1 (read availability); the checkout, 3. Interfaces per consumer:
AvailabilityReader.read(event_id) -> list[SeatFree]andReservationWriter.create(...)— nobody depends on methods they don't use; test fakes are tiny. - utils.py is split by domain; the preventive rule: "utils is only pure stateless functions; if it needs config or I/O, it is a service".
Professor's summary
- Layers are tested with the request-less call; principles, with tests that fail when you violate them.
- Clock, gateway and cache: the first dependencies worth injecting (time, money, network = the non-deterministic parts).
- Protocol + production default: DI in Python without containers; the test fake is the living documentation.
- O is demonstrated by adding a payment method without touching the service: 1 new file, 0 modified.
Next: Lesson 25 — Service patterns.