Module 7 · Testing

Lesson 34 — Integration testing

Real databases in containers: no SQLite pretending to be Postgres.

Published
In this lesson
  1. Exercise 1 — The test environment
  2. Exercise 2 — One seat, one winner
  3. Exercise 3 — The outbox and ledger seams
  4. Exercise 4 — Isolation
  5. Exercise 5 — The guarantees table
  6. Submit

Real Postgres in a container. No solutions.md before submitting.

Exercise 1 — The test environment

  1. Bring up the docker-compose.test.yml (Postgres 16 on 5433 + Redis on 6380) and run the integration suite with DATABASE_URL pointing there. Paste the complete command (env var + pytest).
  2. Lie on purpose: run the outbox's partial-index test (25) against SQLite (override_settings or a SQLite test DB). What happens and why? Document the lie concretely (does the test pass and lie, or fail honestly?).
  3. Add the version check to the conftest: if the test DB isn't Postgres 16, pytest.fail at boot with a clear message (27: fail-fast).

Exercise 2 — One seat, one winner

  1. Implement the concurrency test (ThreadPoolExecutor + transaction=True) with N=2, then N=10 threads over the SAME seat: exactly 1 winner, 9 SeatUnavailable.
  2. Measure: how long does the N=10 test take? Note the number as the baseline (36 will compare it under load).
  3. The nowait variant (10): the same test with select_for_update(nowait=True) — the losers fail IMMEDIATELY with LockNotAvailable. Which checkout UX do you prefer and why? (2 lines: wait-block vs reject-retry).

Exercise 3 — The outbox and ledger seams

  1. The outbox atomicity test with a REAL TX: force a failure in the middle of reservar()'s transaction (a trigger/constraint that explodes) and verify zero published events. No transaction mocks: the DB decides.
  2. The ledger constraint's test (31): insert a duplicate (org, mes) and verify IntegrityError — and that liquidar_mes's code turns it into a skip, not a crash (the get_or_create).
  3. The resumable saga's test (32): create a PENDING saga with intent_id, "kill" the process (simulate with an uncaught exception), resume, and verify ONE charge in the fake gateway with the Idempotency-Key.

Exercise 4 — Isolation

  1. Break isolation on purpose: a test that writes to Redis DB 0 (the dev one) and another that depends on that key. Run the second alone: red? Run the whole suite: green by ordering? Document the smell and fix it (prefix + DB 15).
  2. Run the integration suite with pytest -p no:randomly and then with randomization (pytest-randomly): did any dependent test surface? The test that only passes in order: fix it or kill it.
  3. The conftest's final rule: an autouse fixture verifying that NO test writes to the dev DB (the URL must contain tf_test). How would you implement the guard in the fixture?

Exercise 5 — The guarantees table

  1. Write the guarantees table (§5) for YOUR project with the real state: each row with its test (or "missing"). How many guarantees sold to the business have no policy?
  2. Implement the most critical missing one (pick: advisory lock, ledger's UniqueConstraint, or resumable saga) and paste it.
  3. Run the whole suite and time it per layer: units X s, integration Y s, e2e Z s. Does it meet the budget (10s / 2min / 10min)? If not: which test is in the wrong layer?

Submit

Paste the test compose, the N=10 test with its ms, the outbox atomicity test and the guarantees table with its gaps. Next: Lesson 35 — E2E and contract testing.