Learning Goals
3 min- Define the four testing levels and what each covers.
- Draw and explain the test pyramid.
- Match a real bug to the level that should catch it.
- Avoid the "ice-cream cone" anti-pattern (too many slow tests).
Warm-Up · Zoom Levels
5 minunit one function/class, in isolation (zoomed all the way in) integration a few pieces working together system the whole app running end-to-end acceptance does it meet what the user asked for? (zoomed all the way out)
Each level tests at a different zoom. Unit tests are tiny, fast, and pinpoint bugs precisely. Higher levels catch bugs that only appear when parts combine — but they're slower and vaguer about where the problem is. You need a mix, weighted toward the bottom.
New Concept · The Four Levels & the Pyramid
14 min1. Unit tests
Test one small unit (a function or class) in isolation. Fast (milliseconds), numerous, precise. When one fails, you know exactly what broke.
def add(a, b): return a + b assert add(2, 3) == 5 # a unit test
2. Integration tests
Test that several units work together — e.g., your CRUD functions actually talk to SQLite correctly, or your scraper feeds your parser.
# integration: save then load round-trips through the real DB save_user(db, "Aisyah") assert load_user(db, "Aisyah") is not None
3. System tests
Test the whole application running together, usually through its real interface (HTTP requests to a running server, the full pipeline end to end).
4. Acceptance tests
Test against the requirements: does the system do what the user/business actually asked for? Often written with stakeholders, in plain language.
The test pyramid
/\ few ← acceptance / E2E (slow, brittle, broad)
/ \
/ \ some ← system / integration
/ \
/________\ many ← UNIT tests (fast, cheap, precise)
Write LOTS of fast unit tests, SOME integration, a FEW slow end-to-end.The anti-pattern: the ice-cream cone
________ many slow E2E / manual tests ← painful!
\ /
\ / few unit tests
\ /
\/
A suite dominated by slow, flaky end-to-end tests is slow to run,
hard to debug, and breaks constantly. Flip it: favour unit tests.Worked Example · One App, Four Levels
12 minTake the Level-4 blog app. Here's a test at each level:
# 1. UNIT — the slug helper in isolation assert slugify("Hello World!") == "hello-world" # 2. INTEGRATION — add_post writes to the DB and list_posts reads it back pid = add_post(db, "Title", "Body") assert any(p["id"] == pid for p in list_posts(db)) # 3. SYSTEM — hit the running app's route (Flask test client, L6-36) resp = client.post("/new", data={"title": "Hi", "body": "Hello there"}) assert resp.status_code == 302 # redirect after save # 4. ACCEPTANCE — the user's requirement, in their words # "A visitor can read a published post on the home page without logging in." resp = client.get("/") assert b"Hi" in resp.data
Read the diff
Notice the trade-off: the unit test (slugify) runs instantly and pinpoints exactly what failed. The acceptance test is the most meaningful to a user — but if it fails, you only know "something in the whole stack is wrong". That's why you write many cheap unit tests and just a few broad ones. Where a bug can be caught by a unit test, catch it there.
Try It Yourself
13 minClassify each: "test the price-rounding function", "test checkout charges the card AND emails a receipt", "a user can buy an item from search to confirmation".
Answer
Unit (rounding function), integration (charge + email together), acceptance/system (full purchase journey).
For an app you know, list which tests would be unit, which integration, which E2E. Are you naturally drawn to too many slow ones? Re-balance.
Take a bug that an E2E test would catch and figure out how to catch it with a faster unit or integration test instead. Why is the lower-level test better?
Hint
E.g., "the total on the checkout page is wrong" (E2E) is really "the calculate_total function is wrong" (unit). The unit test runs in 1ms, says exactly what's broken, and never flakes due to browser timing.
Mini-Challenge · Test Plan
8 minFor a to-do app, write a one-page test plan: list 10 tests, assign each a level, and sketch the pyramid (how many of each). Justify why most are unit tests.
Show one possible solution
UNIT (6): add_task returns id; toggle flips done; delete removes;
empty title rejected; due-date parser; sort-by-date
INTEGRATION(3): add then list round-trips DB; mark-done persists;
export writes a valid file
ACCEPTANCE(1): "a user can add, complete, and see a task" (E2E)
Pyramid: 6 unit / 3 integration / 1 E2E — fast feedback,
broad coverage where it's cheap, one slow test for confidence.Non-negotiables: most tests are unit, a few integration, one E2E, and a sentence on why the shape is right.
Recap
3 minFour levels at increasing zoom-out: unit (one piece), integration (pieces together), system (whole app), acceptance (the requirements). The test pyramid says: many fast unit tests at the base, fewer integration, a few slow end-to-end at the top. Avoid the ice-cream cone. Catch each bug at the lowest level you can. Next: designing test cases on paper.
Vocabulary Card
- unit test
- Tests one function/class in isolation — fast and precise.
- integration test
- Tests several pieces working together.
- system / E2E test
- Tests the whole app running end to end.
- test pyramid
- The healthy ratio: many unit, some integration, few E2E.
Homework
4 minPick one of your earlier projects (a game, the blog, a CLI tool). Write a test plan with at least 10 tests labelled by level, drawn as a pyramid. Identify one bug you'd push down from a slow level to a fast one.
Follow the mini-challenge format for your project. The "push a bug down" reflection is the key insight — cheaper, faster, more precise tests beat slow broad ones whenever possible.