Express course · No. 21
A test is just code that checks your code, automatically. People think its job is to prove today's code works — but the real payoff comes later: tests are what let you change, refactor, and add features tomorrow without breaking what already worked. Learn the kinds and the vocabulary, and testing stops being a chore and becomes the thing that lets you move fast.
19 concepts · a diagram for each · cheat sheet at the end
Almost everyone misunderstands what tests are for. They look like a way to prove code works; their real value is deeper and more practical — and seeing it changes how you write them.
The map: 19 concepts in seven chapters
A metro map: first you see all the lines at once, then you ride station by station.
Every concept in the course sits in one of seven chapters. First, why tests exist at all. Then the kinds of test and how to balance them. Then the fakes you need to isolate a piece of code. After that: the craft of a good test, trusting the suite, and putting it all to work.
How to read this course
A passport: the same fields for everyone, so they're easy to compare.
Every concept gets the same treatment: its English name (remember it — it's the term you'll meet in docs and interviews), a picture from life, a diagram, and a card with fields: problem, idea, example, cost, when to use, and related concepts. One idea runs through the whole course: tests are insurance for future changes, not a certificate that the code worked once.
Automated Test — code that checks code
A second person who re-does your sums on a calculator and flags any that don't match — automatic, tireless, and the same every time.
A test is a small piece of code that runs your code with a known input and checks the result is what you expect. "Given 2 and 3, does add return 5?" If yes, the test passes; if not, it fails and tells you. That's the entire mechanic. The power is that it's automatic and repeatable — you can run thousands of these checks in seconds, every time you change anything, without a human re-verifying by hand.
- Problem
Checking by hand is slow and boring — so after each edit nobody re-checks everything.
- Idea
Write the check as code: known input → call → compare with expectation. Result: pass or fail.
- Example
- "
add(2, 3)must return 5" — one line, runs in milliseconds. - Cost
- Tests are code too: they must be written, read, and maintained.
- When to use
- For any code you plan to keep changing.
- Related
Catches Regressions; comes as Unit, Integration, and E2E; runs in the CI Gate.
Safety Net — the real point is change without fear
A safety harness on a climber: it doesn't make the climb, but it lets you attempt a bold move because a slip won't be fatal. You climb harder because you're caught if you fall.
Here's the misunderstanding. Tests aren't mainly about proving today's code is correct — they're about tomorrow. When you change code, refactor it, or add a feature, a good test suite instantly tells you if you broke something that used to work. That safety net is what lets you change a large codebase confidently instead of being afraid to touch anything. Tests are how you keep moving fast without things silently breaking behind you.
Regression — when a change breaks what worked
A row of tripwires through a house: the moment something disturbs a room you weren't even looking at, an alarm goes off — you don't have to patrol every room yourself.
A regression is when a change breaks something that used to work — often somewhere far from what you touched. Without tests, regressions hide until a user finds them. With tests, the moment your change breaks an old behaviour, a test goes red and points at it, right then. This is the everyday payoff: you make a change, run the tests, and instantly know whether you also broke something elsewhere. The suite watches the whole house while you work in one room.
- What it is
- A change breaks behaviour that used to work.
- Catch
It often breaks far from where you edited, in a distant part of the system.
- Example
You changed the cart total — and invoices started showing the wrong amount.
- How to catch it
- A suite of tests on old behaviour, run on every change.
- Tip
- Found a bug? Add a test for it so it never comes back.
- Related
Caught by an Automated Test; guarded automatically by the CI Gate.
A test checks code automatically. Its real value isn't a one-time proof — it's the freedom to change tomorrow without silently breaking what works today.
The foundation of testing is the unit test: a fast, focused check of one small piece of code on its own. Most of your tests will be these, and for good reason.
Unit Test — one small piece in isolation
Checking a single Lego brick is the right shape and size before building with it — not assembling the whole castle to discover one piece was wrong.
A unit test checks one small unit — usually a single function — in isolation from the rest of the system. Give it an input, check its output, nothing else involved. Because the piece is small and standalone, when the test fails you know exactly what broke and where. Unit tests are the microscope of testing: narrow, precise, and pointed at one thing at a time.
- Problem
When a big check fails, you can't tell which of a hundred parts is to blame.
- Idea
Check one function at a time, cut off from the database, network, and everything else.
- Example
calculateTax(100)returns20— no database, no HTTP, in milliseconds.- Cost
- It can't see how the pieces work together — other tests cover that.
- When to use
For most of your tests: business logic, calculations, rules, edge cases.
- Related
The base of the Test Pyramid; isolated by Test Doubles; shaped as Arrange–Act–Assert.
Fast and many — that's the whole idea
A spell-checker that scans the whole document in a blink — so fast you run it constantly, not a slow review you do once a month.
Because each unit test is tiny and touches nothing external, it runs in milliseconds — so you can have thousands and run them all on every change, constantly. That speed is the point: a test suite you can run in seconds gets run all the time, catching breakage the instant it happens. A slow suite gets skipped, and a skipped suite protects nothing. Fast and plentiful is what makes unit tests the workhorse.
Arrange–Act–Assert — set up, act, check
A simple experiment: set up the conditions, perform the one action, then check the result. Clear setup, one move, one verification.
A good unit test has three plain steps — the Arrange–Act–Assert (AAA) pattern: set up the inputs, call the thing you're testing once, then assert the result is what you expected. An assertion is the check itself — "the answer must equal 5." Keeping tests in this clean shape makes them readable and makes a failure obvious. A test that's hard to read is one nobody will trust or maintain.
- Problem
A tangled test: setup, calls, and checks mixed up — you can't tell what it verifies.
- Idea
- Three blocks in order: setup → one action → check (assertion).
- Example
Arrange: a cart with two items. Act:
cart.total(). Assert: the total equals 20.- Cost
- Almost none — just the discipline of one action per test.
- When to use
- Always, in every test — unit, integration, or end-to-end.
- Related
Makes a test readable — and so Living Documentation; the backbone of a Unit Test.
A unit test checks one small piece in isolation, in milliseconds — so you run thousands constantly. Arrange the input, act once, assert the result.
Unit tests check pieces alone, but software is pieces working together. Two broader kinds of test cover that — and the right balance between all three has a shape worth knowing.
Integration Test — pieces working together
After checking each Lego brick, you snap a few together to make sure they actually connect — the bricks were fine alone, but do they fit?
An integration test checks that several parts work correctly together — your code talking to the database, two services exchanging data. Units can each be perfect in isolation and still fail to connect: a mismatched format, a wrong assumption about the other side. Integration tests catch exactly those seams. They're slower than unit tests because they involve more real moving parts, but they verify the joins that unit tests, by design, skip over.
- Problem
- Every part passes its unit tests, yet together they don't work.
- Idea
- Run several real parts together and check the joint between them.
- Example
- A repository writes a user to a real test database and reads it back.
- Cost
Slower than unit tests; needs an environment set up (database, services).
- When to use
For seams: code ↔ database, service ↔ service, your code ↔ a third-party API.
- Related
The middle layer of the Test Pyramid; catches what Over-mocking hides.
End-to-End Test — the whole system, like a user
A dress rehearsal of the entire play, start to finish, on the real stage — not checking lines or props alone, but the whole performance as the audience will see it.
An end-to-end test (E2E) drives the whole system the way a real user would — click the button, fill the form, check the right thing happened across the entire stack. It's the most realistic test and the most valuable confirmation that the product actually works. But it's also the slowest and most fragile: many parts must all be running, and a small UI change can break it. You use e2e tests for the few critical journeys, not for everything.
- Problem
Every part is tested, but nobody knows if the product works as a whole.
- Idea
- Walk a user's path through the whole real stack and check the outcome.
- Example
A browser robot: log in, add an item to the cart, pay, see "order placed."
- Cost
- The slowest and most fragile; breaks on small UI changes.
- When to use
- For a few critical journeys: sign-in, payment, the main flow.
- Related
The tip of the Test Pyramid; too many of them and you get the ice-cream cone.
Test Pyramid — the right balance
A pyramid stands because its base is wide and its top is narrow — flip it onto its point and it topples. The shape itself is the stability.
The test pyramid is the rule of thumb for balance: many fast unit tests at the base, fewer integration tests in the middle, and a handful of slow end-to-end tests at the top. This shape gives you broad, fast coverage cheaply, with just enough realistic checks on top. The anti-pattern — lots of slow e2e tests and few unit tests — is the ice-cream cone, and it's slow, flaky, and painful. Aim for the pyramid, not the cone.
- Problem
Plenty of tests, but the suite is slow and unstable — so people stop running it.
- Idea
- Many cheap fast tests at the bottom, few expensive slow ones at the top.
- Example
Thousands of unit tests, hundreds of integration tests, a dozen e2e for the main flows.
- Cost
- You have to decide deliberately which level checks each thing.
- Anti-pattern
Ice-cream cone — the pyramid upside down: many e2e, few units.
- Related
Layers are Unit, Integration, E2E; extra e2e breed Flaky Tests.
Unit tests check pieces alone, integration tests check them together, e2e tests check the whole system as a user. Keep the pyramid: many fast, few slow.
To test one piece in isolation, you often have to stand in for the real things it depends on. That's what mocks and stubs are — and they're as easy to misuse as they are useful.
Test Double — a fake instead of the real dependency
A flight simulator instead of a real plane: fake instruments and views let you practise the one skill safely, without the cost and risk of actually flying.
To test a piece in isolation, you replace its real dependencies — a database, a payment service, an email sender — with fakes that stand in for them. The general name for any such fake is a test double, like a stunt double in a film. This lets you test your code without the slow, costly, or unpredictable real thing. You don't want a unit test to actually charge a card or send an email; you swap in a fake that pretends to, so the test stays fast, reliable, and self-contained.
- Problem
Real dependencies are slow, cost money, or behave unpredictably — tests using them are fragile.
- Idea
- Swap in an object that behaves like the dependency, but only pretends.
- Example
- Instead of Stripe, a fake that always answers "payment succeeded."
- Cost
- A fake can lie: the real thing may behave differently.
- When to use
For things external, slow, or random: network, payments, email, time.
- Related
Two main kinds — Stub and Mock; gives a Unit Test its isolation; overdone, it's Over-mocking.
Stub vs Mock — canned answer vs checked call
A stub is a cardboard cutout that just says one line on cue; a mock is an actor who also reports back exactly what you said to them and how.
Two flavours of fake get confused. A stub simply returns a fixed, canned answer — "pretend the database returns this user" — so you can test how your code handles that input. A mock also verifies the interaction — "check that my code called sendEmail exactly once, with this address." A stub feeds your code; a mock watches how your code behaves toward it. Reach for a stub to supply data, a mock to assert a side effect happened.
- What it is
- A fake that returns a prepared answer.
- Example
getUser()always returns{ name: 'Ann' }.- What you check
- Your code's output given that data.
- When to use
- When your code needs to get something from a dependency.
- What it is
- A fake that records calls and lets you verify them.
- Example
Check that
sendEmailwas called exactly once, with the right address.- What you check
The call itself: did it happen, how often, with which arguments.
- When to use
When the side effect matters: the email went out, the payment was sent.
Over-mocking — tests that lie
Rehearsing a play entirely with cardboard stand-ins for every other actor — you can nail your lines and still have no idea if the real cast will actually work together.
Fakes are powerful but dangerous in excess. If you mock everything, your test passes against your assumptions about how the other parts behave — not how they actually do. The test goes green while the real integration is broken. That's the over-mocking anti-pattern. So mock the slow, external, or unpredictable dependencies, but don't fake away the very thing you're trying to verify. A test built entirely on mocks can pass while the real system fails — the most dangerous kind of green.
- Symptom
- The test is green, yet the same function fails in production.
- Cause
Everything around it is faked — the test only checks your assumptions about other parts.
- Example
The database mock always says "ok," while the real database rejects the query over a wrong field.
- Cure
Mock only what's external and slow; check the seams with an Integration Test.
- Related
- Misuse of Test Doubles; erodes trust just like a Flaky Test.
Mocks and stubs stand in for real dependencies so you can test in isolation. A stub supplies data; a mock verifies a call. Over-mock, and the test passes while reality breaks.
Beyond the kinds of test, there's the craft of writing good ones — and one discipline, test-driven development, that flips the usual order and changes how you design code.
TDD — write the test first
Drawing the target before you shoot, not after — so you aim at a defined goal instead of painting a bullseye around wherever the arrow happened to land.
Test-driven development (TDD) reverses the usual order: you write the test before the code. The cycle is red, green, refactor — write a failing test for what you want (red), write just enough code to pass it (green), then clean up the code with the test guarding you (refactor). Writing the test first forces you to define exactly what success means before you build, and guarantees the code is testable, because it was born from a test.
- Problem
- Tests get written "later" — and the code comes out hard to test.
- Idea
A failing test first, then the least code, then a clean-up. Round and round.
- Example
The test "empty cart → total is 0" fails; you write
total(); it goes green; you tidy up.- Cost
Unfamiliar and slower at first; hard when the task itself is still unclear.
- When to use
- For logic with clear rules: calculations, validation, bug fixes.
- Related
Yields testable code and Living Documentation; refactoring relies on Test Behaviour.
Test Behaviour, Not Implementation — check what, not how
Judging a chef by whether the dish tastes right, not by which hand they held the knife in — care about the outcome, not the private details of how they got there.
The most important habit for durable tests: check what the code does, not how it does it — its behaviour, not its implementation. Test that getDiscount returns the right price, not that it called three specific internal helpers in order. Tests tied to the outcome survive refactoring — you can rewrite the insides and they still pass. Tests tied to the implementation break every time you tidy the code, which trains people to distrust and delete them. Test the behaviour, and the test stays useful.
- Problem
Tests break on every refactor, even though the behaviour didn't change.
- Idea
- Check only the observable outcome, through the public interface.
- Example
Check that
getDiscountreturned 90 — not which helpers it called inside.- Cost
Sometimes an edge case is harder to reach through input and output alone.
- When to use
- Always, by default.
- Related
Makes refactoring in TDD possible; extra Mocks often tie tests to the implementation.
Living Documentation — tests as docs
A well-written set of tests reads like a list of promises: "given this, it does that" — clearer than any comment about how the code is meant to behave.
A clear test suite doubles as living documentation. Each test states a promise — "given an empty cart, the total is zero" — and because the tests run, that documentation can never go out of date the way comments do; if behaviour changes, the test breaks. So write tests to be read: clear names, one behaviour each, obvious arrange-act-assert. A newcomer should be able to learn what the code does by reading its tests. That readability is worth as much as the checking.
- Problem
- Comments and wikis lie: the code changed, the text stayed old.
- Idea
- Tests are promises written as code. Break a promise — the test goes red.
- Example
A test named "empty cart — total is zero." It reads like a specification.
- Cost
- Time for clear names and one behaviour per test.
- When to use
- Always — especially in code that newcomers read.
- Related
- Rests on Arrange–Act–Assert and Test Behaviour.
Write the test first (red-green-refactor), test behaviour not implementation, and make it readable — so tests survive refactors and double as documentation that can't go stale.
One number and one nuisance decide whether a test suite is actually trustworthy. Misread coverage or tolerate flakiness, and a green suite stops meaning anything.
Code Coverage — what's tested, not how well
A map showing which streets a patrol drove down — useful to spot the neighbourhoods never visited, but driving past a house doesn't mean you checked the locks.
Code coverage is the percentage of your code that runs during the tests. It's a useful guide for finding what's completely untested — the streets never patrolled. But high coverage doesn't mean good tests: code can be executed by a test that asserts nothing meaningful, scoring 100% while verifying little. Use coverage to find blind spots, not as proof of quality. A line being touched is not the same as a line being checked.
- What it is
- The share of lines (or branches) that ran during the tests.
- Useful for
- Showing blind spots — code no test ever ran.
- Catch
It doesn't measure quality: a line can run without any assertion checking it.
- Example
80% coverage, but all the discount logic sits in the uncovered 20% — that's where the risk is.
- When to use
- As a flashlight for gaps, not as a goal.
- Related
Turns into a trap via Goodhart's Law; no substitute for Risk-based Testing.
Goodhart's Law — don't let coverage become the goal
Paying workers per mile of road painted, and watching them paint long, useless lines in empty lots to hit the number — the metric went up, the roads got no safer.
When a number becomes a target, people optimise the number instead of the thing it measured — that's Goodhart's law. Demand "100% coverage" and you get tests written to touch lines, not to catch bugs: hollow tests that assert nothing, gaming the metric. Coverage is a flashlight for finding gaps, not a trophy to maximise. Chase real confidence — do the tests actually catch breakage? — and let coverage be one hint among several, never the goal itself.
- Gist
- When a measure becomes a target, it stops being a good measure.
- In testing
A "no merge without 100% coverage" rule breeds tests without assertions — just for the number.
- Example
A test calls the function and checks nothing — coverage rises, protection is zero.
- Cure
- Ask "would this test catch a breakage?" and keep coverage as a hint.
- Related
- The main trap of Code Coverage.
Flaky Test — worse than no test
A fire alarm that goes off at random for no reason: within a week everyone ignores it completely, so when there's a real fire, no one moves.
A flaky test is one that passes and fails randomly without the code changing — usually from timing, ordering, or a hidden dependency. Flaky tests are poison: when a red result might just be noise, people stop trusting any red, and a real failure gets waved through as "probably just flaky." A test must be deterministic — same code, same result, every time. Fix flaky tests or delete them; a suite you don't trust protects nothing.
- What it is
A test that sometimes passes and sometimes fails, with no code change.
- Causes
- Timing (sleeps, races), test order, a hidden shared dependency.
- Example
A test waits 100 ms for a service; on a busy CI machine it sometimes gives up too early.
- Harm
- It teaches the team to ignore red — and a real failure slips through.
- Cure
Make the test deterministic — or delete it. Never live with a flaky test.
- Related
- Most at home in E2E; wrecks trust in the CI Gate.
Coverage shows what's tested, not how well — don't make it the goal. And a flaky test is worse than none: once red might mean noise, every red gets ignored.
Testing is judgement, not ritual. The skill is testing the things that matter, in the right proportions, and wiring it into how you ship — so the suite earns trust instead of becoming busywork.
Risk-based Testing — test the risky parts, not everything
You triple-check the parachute and the brakes; you don't stress-test the cup holder. Effort goes where failure actually hurts.
Not all code deserves equal testing. Risk-based testing means concentrating effort where a bug would be costly or likely: core business logic, money, security, the tricky edge cases, the parts that change often. Trivial, low-risk code can get less. Chasing tests for everything wastes effort and creates a suite so big and slow nobody runs it. Aim for tests that catch the failures you'd genuinely regret, not for a number — coverage of what matters beats coverage of everything.
- Problem
Testing time is scarce, and trying to cover everything spreads it thin.
- Idea
- More tests where failure is costly or likely; fewer on the trivial.
- Example
Payment maths and access rights — tested densely; getters and simple layout — minimally.
- Cost
- You have to judge risk honestly, not test whatever is easy.
- When to use
- Every time you decide where to spend testing time.
- Related
Replaces chasing Code Coverage; densest as Unit Tests on logic.
CI Gate — tests on every change, automatically
A turnstile that won't open unless your ticket is valid — the check is built into the gate, not left to whether someone remembers to look.
Tests only protect you if they actually run. Wire them into your pipeline so they execute automatically on every change, and block the merge if they fail — the CI gate from the deploy course. This is what turns tests from a thing you might run into a guarantee that nothing broken merges. "Tests or it didn't ship" is the standard worth holding: an untested change going to production is a bet you didn't have to make.
- Problem
The tests exist, but people forget to run them — and broken code lands in main.
- Idea
CI runs every test on every change and refuses the merge until they're green.
- Example
A pull request: CI runs the tests, one fails — the merge button is disabled.
- Cost
- Waiting for CI; flaky tests hurt most here.
- When to use
- In any project with more than one change a week.
- Related
Runs the whole Test Pyramid; can't live with Flaky Tests; catches Regressions before users do.
How it all connects
An orchestra: every instrument matters, but the music only happens together.
The concepts in this course work as one system. The pyramid sets the shape of the suite, TDD writes it, test doubles give unit tests their isolation, and the CI gate runs it all on every change — and that delivers the main prize: changing code without fear. Working against it are flaky tests and over-mocking, which undermine trust in green. And coverage only hints at where the gaps are.
- Does it let you change with confidence — would it catch a regression tomorrow?
- Is it shaped like a pyramid — many unit, some integration, a few e2e?
- Do tests check behaviour, not internal implementation that breaks on refactor?
- Are mocks used for real external deps, not faking away the thing under test?
- Is it fast and deterministic — no flaky tests eroding trust?
- Does it run in CI on every change, blocking broken merges?
- You change code confidently, trusting the suite to catch what you broke.
- Your tests form a pyramid — fast at the base, a few realistic ones on top.
- Tests check behaviour and survive refactors instead of breaking on every tidy-up.
- The suite is fast and deterministic, and you fix or delete flaky tests.
- Tests run in CI on every change, and you treat coverage as a hint, not a target.
Cheat sheet: all 19 concepts
Why we test
- Automated Testtest
Code that checks code: input → call → assertion → pass or fail.
- Regressionregression bug
A change breaks what used to work, often far from the edit.
Unit tests
- Unit Testmodule test
One function in isolation, in milliseconds. You have thousands.
- Arrange–Act–AssertAAA
Set up, do one thing, check the result.
Integration, e2e, and the pyramid
- Integration Testintegration check
Several real parts together — checking the seams.
- End-to-End TestE2E
The whole system as a user sees it. Realistic, but slow and fragile.
- Test Pyramidtesting pyramid
Many unit, fewer integration, a few e2e. Not the ice-cream cone.
Fakes
- Test Doublefake
A stand-in for a slow, costly, or random dependency.
- Stubcanned answer
Returns a prepared answer — feeds your code data.
- Mockcall recorder
Records calls — verifies a side effect happened.
- Over-mockingmocking too much
Everything mocked — the test is green while the real system fails.
TDD and good tests
- TDDred-green-refactor
A failing test first, then the least code, then a clean-up.
- Test Behaviour, Not Implementationblack-box testing
Check the outcome through the public interface — the test survives refactoring.
- Living Documentationexecutable spec
Tests are promises in code that never go stale.
Trusting the suite
- Code Coveragetest coverage
How much code ran in tests. A flashlight for gaps, not a measure of quality.
- Goodhart's Lawmeasure vs target
A measure that becomes a target stops measuring. 100% coverage → hollow tests.
- Flaky Testunstable test
Fails at random. The cure: make it deterministic, or delete it.
Using it well
- Risk-based Testingtest by risk
More tests where failure is costly or likely.
- CI Gatemerge check
Tests on every change; red — the merge is blocked.
Good testing is judgement: test the risky parts, keep the pyramid, check behaviour not implementation, and run it all automatically — so a green suite truly means safe to change.