# Tests are how you change code without fear

Express course on testing. 19 concepts — Unit Test, Integration Test, E2E, Test Pyramid, Stub and Mock, TDD, Code Coverage, Flaky Test, CI Gate and more — each with its English name, a diagram, a card, and an example. The core idea: the point of tests isn't proving correctness once, it's the freedom to change code tomorrow without breaking what works today. A cheat sheet of every term at the end.

- **Author:** Fedor Chishchin
- **Level:** intro
- **Reading time:** ~35 minute read
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/learn/testing-express

---

## 1. Why we test

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.

_Diagram: Seven chapters, 19 concepts. A cheat sheet of all of them waits at the end._

### 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.

_Diagram: A test feeds the code a known input and compares the result with the expectation: match — pass, no match — fail._

#### Automated Test (test, autotest)

- **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.

_Diagram: Every change goes through the suite: green — ship with confidence; red — fix it now, not after a user complains._

### 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.

_Diagram: You changed the cart, and invoices broke — three modules away. The invoice test goes red right away._

#### Regression (regression bug)

- **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**.

> **The framing question:** "Am I writing this test to prove the code works once — or to catch the day someone changes something and breaks this without realising?"

> 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.

## 2. Unit tests

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.

_Diagram: A unit test: input → one function → check the output. Database, network, and disk stay out of it._

#### Unit Test (module test)

- **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)` returns `20` — 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.

_Diagram: The lower the level, the faster the test. A fast suite gets run on every edit — that's why it catches breakage._

### 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.

_Diagram: The three steps of a test: Arrange builds the cart, Act computes the total, Assert compares it with the expectation._

#### Arrange–Act–Assert (AAA)

- **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**.

> **The unit question:** "Can I test this one piece on its own, fast, with a clear arrange-act-assert — so when it fails I know exactly what broke?"

> A unit test checks one small piece in isolation, in milliseconds — so you run thousands constantly. Arrange the input, act once, assert the result.

## 3. Integration, e2e, and the pyramid

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.

_Diagram: An integration test checks the seam: the query to the database and its reply, the JSON between services. Bugs hide in the joins._

#### Integration Test (integration check)

- **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.

_Diagram: An E2E test goes from the user through the UI, the API, and the database — and checks the outcome: was the order placed?_

#### End-to-End Test (E2E)

- **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.

_Diagram: Left, the pyramid: many unit tests, fewer integration, a few e2e. Right, the ice-cream cone — the same pyramid upside down._

#### Test Pyramid (testing pyramid)

- **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**.

> **The pyramid question:** "Do I have many fast unit tests at the base, some integration tests for the seams, and just a few end-to-end tests for the critical journeys — or is my pyramid upside down?"

> 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.

## 4. Fakes: stubs and mocks

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.

_Diagram: The code under test talks to fakes, not to the real Postgres, Stripe, and mail server — those are slow, cost money, or really send things._

#### Test Double (fake)

- **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.

_Diagram: A stub supplies data, and the test checks your code's output. A mock records the call, and the test checks the call itself._

#### Stub (canned answer)

- **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.

#### Mock (call recorder)

- **What it is:** A fake that records calls and lets you verify them.
- **Example:** Check that `sendEmail` was 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.

_Diagram: In the test, everything is mocked — and it's green. In production, the real database and API — and it all fails._

#### Over-mocking (mocking too much)

- **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**.

> **The mock question:** "Am I faking a genuinely external dependency to stay fast and isolated — or have I mocked so much that the test only checks my own assumptions?"

> 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.

## 5. TDD and good tests

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.

_Diagram: The TDD cycle: red (the test fails) → green (the least code that passes) → refactor (clean up, tests stay green) → again._

#### Test-Driven Development (TDD, red-green-refactor)

- **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.

_Diagram: A good test looks at what goes into getDiscount and what comes out. A bad test reaches for the internal helpers — and breaks on every refactor._

#### Test Behaviour, Not Implementation (black-box testing)

- **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 `getDiscount` returned 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.

#### Living Documentation (executable spec)

- **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**.

> **The TDD question:** "Does this test describe the behaviour I promised — readable, outcome-focused — so it survives a refactor and teaches the next person what the code does?"

> 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.

## 6. Coverage and flaky tests

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.

_Diagram: 75% coverage: 6 of 8 lines ran. But only 3 are really checked by an assertion._

#### Code Coverage (test coverage)

- **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.

_Diagram: A measure becomes a target — and hollow tests are born: the number goes up, the bugs stay._

#### Goodhart's Law (measure vs target)

- **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.

_Diagram: Same code, six runs — and two random failures. After that, every red gets dismissed as noise._

#### Flaky Test (unstable test)

- **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**.

> **The trust question:** "Is my suite actually trustworthy — coverage used as a hint not a goal, and zero flaky tests — so that a green run genuinely means safe to ship?"

> 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.

## 7. Using it well

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.

_Diagram: The costlier the failure and the likelier the breakage, the more tests. Money, security, and core logic sit in the top-right corner._

#### Risk-based Testing (test by risk)

- **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.

_Diagram: Every change goes through CI: all tests green — merge; even one red — the merge is blocked._

#### CI Gate (merge check)

- **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.

_Diagram: The pyramid, TDD, and test doubles build the suite; the CI gate runs it and lets you change without fear. Flaky tests and extra mocks hit trust; coverage only hints._

#### Before you trust a test suite

- **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?

#### Signs you test well

- 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 Test** (test): Code that checks code: input → call → assertion → pass or fail.
- **Regression** (regression bug): A change breaks what used to work, often far from the edit.

### Unit tests

- **Unit Test** (module test): One function in isolation, in milliseconds. You have thousands.
- **Arrange–Act–Assert** (AAA): Set up, do one thing, check the result.

### Integration, e2e, and the pyramid

- **Integration Test** (integration check): Several real parts together — checking the seams.
- **End-to-End Test** (E2E): The whole system as a user sees it. Realistic, but slow and fragile.
- **Test Pyramid** (testing pyramid): Many unit, fewer integration, a few e2e. Not the ice-cream cone.

### Fakes

- **Test Double** (fake): A stand-in for a slow, costly, or random dependency.
- **Stub** (canned answer): Returns a prepared answer — feeds your code data.
- **Mock** (call recorder): Records calls — verifies a side effect happened.
- **Over-mocking** (mocking too much): Everything mocked — the test is green while the real system fails.

### TDD and good tests

- **TDD** (red-green-refactor): A failing test first, then the least code, then a clean-up.
- **Test Behaviour, Not Implementation** (black-box testing): Check the outcome through the public interface — the test survives refactoring.
- **Living Documentation** (executable spec): Tests are promises in code that never go stale.

### Trusting the suite

- **Code Coverage** (test coverage): How much code ran in tests. A flashlight for gaps, not a measure of quality.
- **Goodhart's Law** (measure vs target): A measure that becomes a target stops measuring. 100% coverage → hollow tests.
- **Flaky Test** (unstable test): Fails at random. The cure: make it deterministic, or delete it.

### Using it well

- **Risk-based Testing** (test by risk): More tests where failure is costly or likely.
- **CI Gate** (merge 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.

End of express course · 7 chapters · 19 concepts

Next comes practice: take one important function and write a few unit tests for it — arrange, act, assert — then deliberately break the function and watch a test go red and tell you exactly what. Then add a test for the next bug you find, so the suite grows along the failures that actually happen. But hold one idea above the rest: tests aren't about proving today's code works. They're the safety harness that lets you change tomorrow without fear — and that freedom to change confidently is what lets good software keep growing instead of slowly calcifying.
