What to test, and what not to bother with
Most advice about testing is either "test everything" or a number — eighty percent coverage, the test pyramid, one assertion per test. None of it survives contact with a real project, because none of it answers the only question that matters: which tests earn their keep?
A test costs you three times. Once to write it. Every time it runs, in time you wait. And every time the code changes and the test has to change with it — which is the expensive one, and the one nobody counts.
The test that is worth writing
Ask two questions about any test you are about to write.
If this broke, would anybody notice? Not "is it correct" — would a customer,
or the shopkeeper, or the accountant notice? If cartTotals is wrong, somebody
is billed a number they were never shown. That is worth a test. If a heading is
text-lg instead of text-xl, nobody will ever ring the shop about it.
Could this break without the code changing? A pure function only breaks when you edit it. Anything that depends on the database, on time, on concurrency or on another process can break while you sleep — a migration, a timezone, a second customer. Those deserve tests at a level where the dependency is real.
Together they explain why this course tests money, stock, the state machine and the dates very hard, and tests the layout not at all.
What each layer can prove
The layers are not a hierarchy of goodness. Each one can prove something the others cannot, and is wasted on everything else.
| Layer | What only it can prove | Cost |
|---|---|---|
| Unit | arithmetic, rules, transformations | milliseconds |
| Service | branches and refusals, with fakes | milliseconds |
| API end-to-end | transactions, locks, cookies, signatures | seconds, needs PostgreSQL |
| Component | what a person actually sees rendered | milliseconds |
| Browser (Playwright) | the whole thing joined up | tens of seconds, needs everything |
The Kirana Store ends up with 124 tests, and the shape is lopsided on purpose: 110 of them run in under a second, 29 need a database, and 7 need a browser.
That is the pyramid, but arrived at by asking what each test proves rather than by obeying a diagram.
Do not test the same thing twice
This is the rule that keeps a suite from rotting.
cartTotals is unit tested exhaustively — every boundary of the free-delivery
threshold, an empty cart, a hundred lines. So the API end-to-end test does
not re-check the arithmetic. It checks that the order's total matches the
cart's, which is a different claim: that the two halves agree.
And the Playwright test checks neither. It checks that somebody can buy something.
When a test fails, you should be able to tell from which test it was what broke. Three tests covering the same ground all go red together and tell you nothing.
The tests this course does not write
Worth listing, because leaving them out is a decision.
No snapshot tests. A snapshot asserts "the output is what it was", which is
not a claim about correctness. In practice somebody changes the markup, the
snapshot fails, they run --update without reading it, and the suite has
learned nothing while costing everybody time.
No tests of Prisma. findMany works. Testing that a query builder builds
queries is testing somebody else's library.
No tests that mock the thing under test. If a service test mocks Prisma and
then asserts expect(prisma.variant.updateMany).toHaveBeenCalled(), it passes
when the SQL is wrong, when the condition is wrong, and when the database
rejects it. It tests the mock.
That last one is why stock release is proved end to end in this course, against a real PostgreSQL, and not in a unit test.
No coverage target. Coverage tells you which lines ran, not whether anything was checked. A test that calls every function and asserts nothing scores 100%. It is a useful map of what is untested; it is a terrible goal.
The one number that matters: how long it takes
npm run test → 110 tests, about 4 seconds
npm run test:e2e → 29 tests, about 3 seconds, needs PostgreSQL
playwright test → 14 runs, about 15 seconds, needs everything
A suite you run is worth ten times a suite you do not. The moment the fast tests take a minute, people stop running them before pushing, and the whole thing becomes CI's problem — which means finding out twenty minutes later instead of immediately.
So the split is by what it needs, not by what it is called. npm run test
needs nothing and runs on every save. Everything else runs before a push and in
CI.
A red test must mean something
The worst outcome in testing is not an untested bug. It is a suite nobody trusts.
A flaky test — one that fails sometimes for reasons unrelated to the code — teaches everybody to re-run the build. Once that habit exists, a real failure is re-run too, and the suite has become worse than useless: it costs time and catches nothing.
So: a flaky test is a bug in the test, and it gets fixed or deleted the same day. There is no third option. The next lessons show two real ones from this project — an ambiguous selector and a race after a sign-in — and neither was the application's fault.
What to write first on a project that has none
In order:
- The money. Whatever computes what somebody is charged.
- The rules. The state machine, the permissions, what is allowed.
- One end-to-end path through the thing the business exists to do. For a shop, buying something.
- A test for each bug you fix, from then on.
Number four is the one that compounds. Every bug you fix is proof that you can be wrong about that thing, and a test there is a test aimed at somewhere you have already been caught out.
Check your work
The three costs of a test: writing it, running it, and changing it when the code changes — the last being the expensive one.
The two questions: would anybody notice if this broke, and could it break without the code changing?
Why the layers are not a hierarchy: each proves something the others cannot, and is wasted on the rest.
Why not to test the same thing twice: a failure should tell you what broke, and duplicated coverage goes red together saying nothing.
Why no snapshot tests: they assert that the output is unchanged, not that it is right, and updating them is a reflex.
Why mocking Prisma and asserting the call proves nothing: it passes with wrong SQL, a wrong condition and a rejecting database.
Why no coverage target: a test that asserts nothing scores 100%. Coverage maps what is untested; it is not a goal.
Why speed matters: a suite people stop running before pushing has become CI's problem, and CI answers twenty minutes late.
Why a flaky test is an emergency: it teaches everybody to re-run the build, and then real failures get re-run too.
What to write first: the money, the rules, one end-to-end path, and then a test for every bug you fix.
Practice
- Open the Kirana Store's test folders and count how many tests need a database. Explain why each one does.
- Pick a test in
packages/sharedand work out what would have to break for it to fail. Then do the same for a Playwright test. - Find a function in the project you would not test, and write down why.
- Delete an assertion from
cartTotals's tests and see whether anything else catches it. - Run
npm run test:covon the API. Find a file with high coverage and decide whether it is actually well tested. - Time
npm run test. Decide what number would make you stop running it before every push. - Write down the last bug you fixed in any project, and the test you would add for it.
- Find a test in an open-source project that asserts a mock was called. Decide what it would catch.
- Take one end-to-end test from this project and argue for deleting it. Then argue against.
- Write the four things you would test first on a project you have never seen.
Next: unit testing the services, without a dependency injection container in sight.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship