Testing foundations

Choose what a test proves

Start from one risk and one observable behavior instead of writing tests merely to increase a coverage number.

A test is useful when it can fail for a reason you care about. So before writing one, I pick a behavior that would hurt someone if it broke. A user, or a developer on the team. Then I write the test that catches that break.

Throughout this course we’ll test a small Books API. It stores books with a title, an author, and an ISBN, and it has a page where a reader can add a book. Small, but it touches every layer we want to test.

Three kinds of tests

Unit tests check logic that always gives the same output for the same input, like “the title gets trimmed”. Integration tests check the places where real components meet, like our code and the database. Browser tests check a few user flows end to end, in a real browser.

The names matter less than one question: which boundary does this test cross, and which failure can it point at?

Start from a sentence

I write the sentence first, then the test. “Given this state, when this action happens, then I can observe this result.”

The last part is the oracle: the evidence that tells you the behavior is correct. If the oracle only checks that nothing threw, the route can return the wrong book and the test stays green. That’s a test that can’t fail for a meaningful reason.

Match the risk to the boundary

Take the risk “a duplicate ISBN replaces an existing book”. That one lives at the database boundary. An integration test against the real uniqueness constraint proves it. A unit test with a fake repository that you taught to reject duplicates proves nothing about the real database.

The risk “the title is trimmed” is pure logic. A fast unit test is enough.

The risk “a reader can add a book from the page” needs labels, JavaScript, HTTP, and storage to work together. That one deserves a browser test.

Too low, too high

Pick a boundary that’s too low and you get false confidence. Every piece passes on its own, and the wiring between them is broken.

Pick a boundary that’s too high and you get slow, vague failures. A browser test says “Add book failed” and you still don’t know if validation or storage caused it.

My rule: use the cheapest boundary that can expose the risk. Add a broader test only when the integration itself is the risk.

Try this on the Books API: list five risks. For each, write the observable failure, the boundary that must be real, and the cheapest test that proves it. If you can’t name the evidence, the idea is still too vague to test.

Lesson completed