Learning Goals
3 min- Explain why all-green unit tests can hide integration bugs.
- Write a test that exercises two or more real components together.
- Decide what to use real vs mock in an integration test.
- Place integration tests correctly in the pyramid.
Warm-Up · Two Green Units, One Broken App
5 min# parser.py — returns price as a STRING def parse(line): return {"item": "roti", "price": "1.50"} # tested, green # cart.py — expects price as a FLOAT def add_line(cart, parsed): cart.total += parsed["price"] # "1.50" + float → TypeError!
Both units pass their own unit tests in isolation. But together they crash — the parser hands a string where the cart wants a float. Only a test that runs both catches this.
Unit tests check each part alone; integration tests check the contracts between parts — data shapes, types, call order. Most production bugs live at these seams, not inside well-tested units.
New Concept · Testing the Seams
14 minWhat an integration test exercises
unit test: function A alone (mock everything A talks to)
integration test: A + B + C running together for real
→ catches mismatched data, wrong call order,
config issues, real DB/file behaviourAn integration test of parser → cart
# test_integration.py from parser import parse from cart import Cart, add_line def test_parse_then_add(): cart = Cart() parsed = parse("2x roti @1.50") # REAL parser add_line(cart, parsed) # REAL cart assert cart.total() == 3.00 # the SEAM works end to end
This runs both real components. If the parser's output shape doesn't match what the cart expects, the test fails — catching the bug the unit tests missed.
What to keep real vs mock
KEEP REAL the components you're testing the integration OF
(parser + cart, repo + DB, route + service)
MOCK the true external edges you don't control
(third-party APIs, payment gateways, email)Integration tests deliberately use MORE real objects than unit tests — that's the point. You only mock the genuine outside world.
Place in the pyramid
/\ few E2E / \ / IT \ some INTEGRATION ← this lesson /______\ many unit Fewer integration than unit (they're slower), more than E2E.
They're slower — organise them
# mark them so you can run fast units separately (Lesson 17) import pytest @pytest.mark.integration def test_full_flow(): ... # run units only: pytest -m "not integration" # run integration only: pytest -m integration
Worked Example · Repo + DB Integration
12 min# integration: the repo + a REAL (in-memory) database + the service import sqlite3, pytest from repo import UserRepo from service import register_user # uses the repo @pytest.fixture def repo(): con = sqlite3.connect(":memory:") con.execute("CREATE TABLE users (name TEXT, email TEXT UNIQUE)") yield UserRepo(con) con.close() @pytest.mark.integration def test_register_flow(repo): # exercises service → repo → real SQL, all together register_user(repo, "Aisyah", "a@x.com") assert repo.find("a@x.com") == "Aisyah" @pytest.mark.integration def test_duplicate_email_rejected(repo): register_user(repo, "Aisyah", "a@x.com") with pytest.raises(Exception): # the UNIQUE constraint fires register_user(repo, "Other", "a@x.com")
$ pytest -m integration -v test_register_flow PASSED test_duplicate_email_rejected PASSED 2 passed
Read the diff
These tests run the service, the repo, AND real SQL against an in-memory DB. The duplicate-email test even relies on the database's real UNIQUE constraint firing — something a mocked DB would never catch. That's the value: integration tests verify the whole chain, including behaviour that only emerges when real components meet.
Try It Yourself
13 minPick two of your components that pass data between them (parser→processor, loader→analyser). Write an integration test running both for real.
Introduce a type mismatch between two components (str vs int). Show the unit tests still pass but the integration test fails. Fix the contract.
Tag your integration tests with @pytest.mark.integration. Provide commands to run units-only (fast) and integration-only.
Mini-Challenge · End-to-End Through Your Pipeline
8 minTake a Level-4 data pipeline (load CSV → clean → store → query). Write one integration test that runs the whole chain on a tiny real dataset and asserts the final query result. Mock only any external API; keep the file and DB real (use tmp_path + in-memory SQLite).
Recap
3 minUnit tests check parts alone; integration tests check the seams between them — where most real bugs live. Keep the components-under-test real; mock only the true external edges. Integration tests are slower, so mark them and run them separately from the fast unit loop. Next: a key integration target — testing a Flask app.
Vocabulary Card
- integration test
- Tests multiple real components working together.
- seam / contract
- The boundary where components exchange data; a common bug site.
- real vs mock (in integration)
- Keep the integration target real; mock only external systems.
- integration marker
- A pytest tag to run slower integration tests separately.
Homework
4 minFor a multi-component project of yours, write 3 integration tests that exercise real components together (keep external APIs mocked). Tag them @pytest.mark.integration. Demonstrate one seam bug caught by integration but missed by units. Provide the fast vs full run commands.
Model on the repo+DB+service example. The L4 pipeline (load→clean→store→query) makes a great real-world integration target.