fedorthinks

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

§ 01

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.

1 · Why we testAutomated testRegression2 · Unit testsUnit testArrange–Act–Assert3 · PyramidIntegration testE2E testTest pyramid4 · FakesTest doubleStubMockOver-mocking5 · CraftTDDTest behaviourLiving docs6 · TrustCode coverageGoodhart's lawFlaky test7 · PracticeRisk-based testingCI gateCheat sheetall terms, at the end
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.

Inputadd(2, 3)Code under testadd()Assertionexpect 5PASSgot 5FAILgot 6automatic · repeatable · thousands of checks per run
A test feeds the code a known input and compares the result with the expectation: match — pass, no match — fail.
ConceptAutomated 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.

Changerefactor · featureTest suiteruns in secondsgreen ✓old behaviour okred ✗broke old codeShip itwith confidenceFix it nowbefore users see
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.

CodebaseCartyou changed thisPricingCheckoutInvoicesbroke silentlyinvoice test ✗a tripwire in a room you weren't looking at
You changed the cart, and invoices broke — three modules away. The invoice test goes red right away.
TermRegression· 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.

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.

§ 02

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.

isolationUnitone functioninputexpected?not touched:DatabaseNetworkDisk
A unit test: input → one function → check the output. Database, network, and disk stay out of it.
ConceptUnit 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.

time per testunitmillisecondsintegrationslowere2eslowestfast suite → run on every change → breakage caught instantly
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.

1 · Arrangecart = new Cart()cart.add(item, 2)2 · Acttotal = cart.total()3 · Assertexpect(total) .toBe(20)set up · do one thing · check the resultassertion = the check itself
The three steps of a test: Arrange builds the cart, Act computes the total, Assert compares it with the expectation.
PatternArrange–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.

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

§ 03

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.

seamYour codeDatabasequeryrowsService AService BJSONreplybugs hide in the joins: format mismatch, wrong assumptions
An integration test checks the seam: the query to the database and its reply, the JSON between services. Bugs hide in the joins.
ConceptIntegration 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.

the whole stack, really runningUserclicks, typesUIbrowserAPIserverDatabaseOrder placed?assertmost realistic · slowest · most fragileuse it for the few critical journeys
An E2E test goes from the user through the UI, the API, and the database — and checks the outcome: was the order placed?
ConceptEnd-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.

E2EIntegrationsomeUnitmany, fastTest pyramid ✓E2Emany, slowIntegrationunit (few)Ice-cream cone ✗
Left, the pyramid: many unit tests, fewer integration, a few e2e. Right, the ice-cream cone — the same pyramid upside down.
PatternTest 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.

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.

§ 04

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.

test doublereal thingCode under testFake DBFake paymentsFake emailPostgresslowStripecosts moneySMTPsends for real
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.
ConceptTest 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.

Stubsupplies dataStubgetUser()Your codereturns { name: 'Ann' }test checks your code's outputMockverifies the callYour codeMocksendEmail()expect(sendEmail) .calledOnceWith(addr)test checks the call happened
A stub supplies data, and the test checks your code's output. A mock records the call, and the test checks the call itself.
TermStub· 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.
TermMock· 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.

in the test: everything mockedCodeMock DBMock APIPASS ✓in production: the real partsCodeReal DBReal APIFAIL ✗green test, broken system — the most dangerous green
In the test, everything is mocked — and it's green. In production, the real database and API — and it all fails.
Anti-patternOver-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.

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.

§ 05

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.

Redwrite a failing testGreenjust enough codeRefactorclean up, stay greenrepeat
The TDD cycle: red (the test fails) → green (the least code that passes) → refactor (clean up, tests stay green) → again.
PracticeTest-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.

getDiscount()helperhelperhelperinternals (free to change)inputcart, couponprice90✓ check the outcome✗ test coupled to internals — breaks on every refactor
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.
PrincipleTest 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.

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

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.

§ 06

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.

12345678executed + assertedexecuted, nothing assertednever executedcoverage: 6 of 8 lines (75%)really checked: 3 of 8a touched line is not a checked line
75% coverage: 6 of 8 lines ran. But only 3 are really checked by an assertion.
MetricCode 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.

Coveragea measurebecomes“100% or no merge”a targetHollow testsassert nothingthe number went up — the bugs stayedcoverage is a flashlight, not a trophy
A measure becomes a target — and hollow tests are born: the number goes up, the bugs stay.
LawGoodhart'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.

same code, nothing changed — six runsPASSrun 1FAILrun 2PASSrun 3PASSrun 4FAILrun 5PASSrun 6red might be noise → people stop trusting any reddeterministic: same code → same result, every time
Same code, six runs — and two random failures. After that, every red gets dismissed as noise.
TermFlaky 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.

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.

§ 07

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.

Test wellrare but costlyTest hardestmoney · security · core logicTest lightlytrivial, low-risk codeTest someedge cases · frequent changeshigh costlow costunlikely to breaklikely to breakeffort goes where failure actually hurts
The costlier the failure and the likelier the breakage, the more tests. Money, security, and core logic sit in the top-right corner.
PracticeRisk-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.

Pusha changeCI pipelineruns every testGateall green?Merge ✓into mainBlocked ✗fix firsttests or it didn't ship
Every change goes through CI: all tests green — merge; even one red — the merge is blocked.
PracticeCI 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.

Test pyramidshapes the suiteTDDwrites tests firstTest doublesisolate unitsTest suiteunit · integration · e2eOver-mockingfalse greenChange without fearregressions caughtFlaky testsdestroy trustCoveragehints at gapsCI gateruns on every change
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 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.

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.