Test-driven Development
Method
Writing the test for the behaviour you're about to build first, then filling in the implementation until that test passes.
Why it exists
Tests planned for "later" mostly don't get written. Once a feature starts working, the next piece of work is already waiting, and re-confirming something that already functions is what gets postponed.
Even when they do get written, another problem remains. Pagedeck's billing calculation had it. Theo finished the implementation, added tests afterwards, and everything passed. Two months later, accounts on the free plan were charged. The calculation was wrong and the tests were wrong in the same way. They had been written while reading the code, so they recorded what the code did — which froze the incorrect behaviour in place alongside the correct parts.
Reversing the order solves both at once. A test written first can't be postponed, and it can't be copied from an implementation that doesn't exist yet, so the question of what the correct behaviour is has to be answered before the code gets written.
How to run it
Repeat three short steps: make a failing test, make it pass, then tidy up. A cycle usually runs five to fifteen minutes.
- Write the next behaviour as one sentence. "This month's charge for a free plan account is zero" — input and result both visible. One per cycle.
- Write the test for that sentence and run it. It has to fail at this point. Passing with no implementation means the test is checking something other than what you intended, and this is the step people skip most often.
- Implement only enough to pass. Don't add handling you expect to need later; when you do need it, a new test will ask for it. Passing a free-plan test may take a single line returning zero, and however trivial that line looks, the calculation grows on its own once the monthly-billing test arrives in the next cycle.
- Tidy up once it passes. This is where refactoring — changing structure while behaviour stays fixed — belongs. The test you just wrote is the safety net, so breaking something while tidying shows up immediately.
- Return to step one. Once a cycle runs past an hour the benefit evaporates, because the failure surface has widened and you're back to searching for where things diverged — which is the situation the short cycle existed to avoid.
- Note down cases that occur to you and don't handle them now. Writing billing code surfaces cancellation months and annual discounts; implementing either one breaks the cycle. Park them in a list and take one per cycle. A long list isn't a problem — it's the feature's real number of cases becoming visible.
Tests here are usually unit tests, each isolating one function or module rather than exercising the whole application. They have to be fast to sustain short cycles; when results take longer than a few seconds, people start skipping steps.
When it fits and when it doesn't
It suits code with clear rules and distinct inputs and outputs: billing calculations, permission decisions, format conversion. Such code has many cases, and confirming each one through the interface means constructing the conditions by hand every time — creating an account, setting a plan, waiting for a billing date — so writing the test first is actually faster.
It also suits bug fixes. Write a test that reproduces the bug, watch it fail, then fix it, and you're left with evidence both that it's fixed and that it won't return silently. The reproduction test is also the clearest way to show a reviewer what was actually wrong.
There are four limits.
- It doesn't fit interface or visual work. Whether a button sits in the right place or a colour works can't be written as an assertion. That part stays with human review, and no amount of cycling will substitute for someone looking at the screen.
- During exploration it only adds cost. If you're changing the structure three times a day looking for a direction, the tests get rewritten three times too. Better to find the direction with code you intend to discard, then rewrite the part you're keeping this way — and say out loud that the code is disposable, or it ships.
- Code that depends on outside systems needs scaffolding. Payment providers and mail delivery have to be stood up as stand-ins, and when a stand-in drifts from the real thing the tests pass while production breaks. Keeping a small number of tests that talk to the real service covers that gap.
- Tests bound to implementation details break every time you tidy. Assertions about internal function names and call counts turn red the moment structure changes. Write them against behaviour observed from outside, or step four stops being possible.
How it sounds at work
"I'll write the test first. If it passes now, the test is wrong."
Not skipping the step that confirms the failure.
"Let's write a reproduction test for this bug before fixing it."
Capturing the fix and the protection against recurrence in one go.
"I'm not doing this for the interface layer, just the calculation."
Partial adoption is the assumption, not a compromise.
After the billing incident, Theo rewrote the calculation this way. Listing the cases first produced fourteen: free plan, monthly billing, annual billing, upgrades mid-period, downgrades mid-period, cancellation months, months with a refund attached. Moving those fourteen into tests one cycle at a time took two days, and three more cases turned out to have been implemented incorrectly along the way, none of which had produced a support ticket yet. The free-plan charge that started the incident was one of them.
What people get wrong
The most common reading is that this is an exercise in raising coverage. Tests do accumulate, so it looks that way. The purpose, though, is design and feedback. Writing the test first forces you to decide how the code gets called from outside, and difficulty calling it is the signal to change the structure. Test coverage is a by-product of the process, and making that number the goal pushes teams toward tests that pass without asserting anything.
The other belief is that all code has to be written this way. The name covers development in general, so it reads as universal. In practice, teams that use it don't use it for interface assembly or configuration. They use it where the rules concentrate and being wrong costs money or exposes data, and everything else gets tests added afterwards where they're needed, usually after the first bug shows which parts deserve them. At Pagedeck about a fifth of the code was written this way, and that fifth is billing, permissions, and usage aggregation. Treating it as all-or-nothing is the most common reason teams never start at all.
Where to go next
Last updated 2026-09-16