Lesson 3 of 9 · 22m
Integration Tests: Where the Bugs Live
Unit tests tell you parts work; integration tests tell you they work together — the database boundary, the HTTP boundary, and what 'realistic' actually means.
"The unit tests pass" and "the service works" are different claims. Integration tests are where you verify the seams — the ORM actually talks to Postgres, the HTTP handler actually deserializes the request body, the queue consumer actually gets the message it expects.
Test at the boundary, not the whole stack
A good integration test exercises one external dependency at a time:
def test_repository_round_trip(db_session):
repo = OrderRepository(db_session)
saved = repo.save(Order(total=100))
assert repo.get(saved.id).total == 100 # real DB, real SQL — not a mockTestcontainers, a real Postgres in CI, or an ephemeral database per suite — the point is testing against the real dependency's behavior, not a mock of your assumptions about it. A mock can't tell you that LIKE is case-insensitive but ILIKE exists, or that Postgres transactions have savepoints.
The HTTP boundary
For a REST service, the integration test exercises the whole request cycle:
def test_post_order_returns_201(client, db):
resp = client.post("/orders", json={"sku": "W-1", "qty": 2})
assert resp.status_code == 201
assert db.query(Order).count() == 1 # verify the side effect, not the route internalsTest the contract (status, headers, body shape) and the side effect — not middleware internals. The service layer's job is the logic; the handler's job is the translation.
Contracts over compatibility
When two services talk, the dangerous bug is a drifted contract — the consumer expects email, the producer sends emailAddress. Contract testing (Pact, OpenAPI conformance) pins both sides to a shared schema so a breaking change fails in CI instead of at runtime.
Field notes
- The test database should look like production — same schema, same indexes, seeded with enough data to make queries realistic. A one-row test table hides cardinality bugs.
- Use
db.rollback()or a fresh schema per test for isolation — tests that share state are order-dependent and lie about each other. - If an integration test needs mocking, it's probably testing too many things at once — split it.