What does writing the test first mean?
You decide what the code should do, write a test that says so, and run it before the code exists. The test fails, because the behaviour isn't there yet. Then you write the code, and the test passes. The order sounds backwards the first time you meet it, and it changes how you work in useful ways.
The name for the full practice is test-driven development, often shortened to TDD. Its loop has three steps: red, green, refactor. Red is a failing test for one small piece of behaviour. Green is the smallest change to the code that makes every test pass. Refactor is tidying the code, with the tests still green, before you start the next loop.
The starting files have one hire rule with one test: two hours cost 12. Spoke and Chain wants two new rules, and you'll add each one through the loop.


Why does seeing the test fail first matter?
Because a test you have never seen fail might be unable to fail. You met this in earlier lessons: a missing expect(), an assertion on the wrong value, a matcher like toBeDefined() that accepts anything. Each of those passes against correct code, which is why nobody notices. When the test runs before the code exists, a test that passes straight away is a warning sign you can't miss.
The failure also tells you something. Write expect(hirePrice(1.5)).toBe(12) and Results shows Received 9. That confirms the test calls the right function, reaches the assertion and compares the right thing. When you later change the code and the test turns green, you know the change made the difference.
Writing tests after the code skips that check. It is possible to write good tests afterwards, and most real tests are written that way. It takes discipline: break the code on purpose, watch the test fail, then put the code back. Test-first builds that step into the routine.
How do you take the first red step?
Pick the smallest piece of the new rule you can state as one example. "Any part of an hour is charged as a full hour" becomes "1.5 hours cost 12". Write that as a test, with the expected value worked out from the rule, and run it.
Read the failure before you touch the code. Expected 12, Received 9 is the failure you predicted: the code multiplies 1.5 by 6. If the failure is different, such as "hirePrice is not a function" or a syntax error, fix that first. The red step only counts when the test fails for the reason you expected.
test('a part hour is charged as a full hour', () => {
expect(hirePrice(1.5)).toBe(12);
});
// Expected: 12
// Received: 9How little code should the green step add?
Only enough to make the tests pass. For the part-hour rule, wrap the hours in Math.ceil(): return Math.ceil(hours) * HOURLY;. Every test turns green. Resist adding the cap at the same time, even though you know it is coming. Without its own red test, the cap would arrive untested, and you wouldn't know whether your cap test can fail.
Small steps keep failures easy to read. If you change three things and a test fails, you have three suspects. If you change one, you have one. When a green step turns into a long struggle, that is a sign the test asked for too much at once. Undo, and write a smaller test.
Then take the next loop. Write expect(hirePrice(8)).toBe(30). It fails with Received 48. Add const DAY_CAP = 30; and return Math.min(Math.ceil(hours) * HOURLY, DAY_CAP). Green again.
What happens in the refactor step?
With every test green, you can improve the code's shape without changing what it does: rename a variable, pull a calculation into a well-named constant, remove duplication. The tests are your safety net. Run them after each small change, and if one turns red, undo that change.
In the hire code, a good refactor is splitting the calculation in two: const charged = Math.ceil(hours) * HOURLY; and then return Math.min(charged, DAY_CAP);. The behaviour is identical and the second line now reads like the rule. Refactoring is also the moment to look at the tests themselves. Are the names clear? Is there a missing boundary?
const HOURLY = 6;
const DAY_CAP = 30;
export function hirePrice(hours) {
const charged = Math.ceil(hours) * HOURLY;
return Math.min(charged, DAY_CAP);
}Why add a test at 6 hours?
Because that is where the cap first changes the answer. Five hours at 6 is exactly 30, so the cap and the hourly price agree there. Six hours would be 36 without the cap, so 6 is the first whole number of hours where the cap has to do its job. A test at 8 hours proves the cap exists. A test at 6 proves it starts in the right place.
Check my work tries a broken copy that only caps hires longer than 6 hours: return hours > 6 ? DAY_CAP : charged;. Your 8 hour test still passes against it. Only a test at 6 notices, because the broken copy charges 36.
Boundary tests like this one are cheap and catch the off-by-one mistakes that slip through tests chosen far from the edge. Test-first makes them easier to think of, because you are writing examples from the rule before any code exists to distract you. Ask: where does the rule switch from one answer to another? Put a test at that point.
When is test-first worth it, and when is it not?
It pays off most for rules with clear inputs and outputs: prices, dates, validation, permissions, anything where you can write the expected answer before the code. It is also a good way to fix a bug, which the next lesson covers: write a test that reproduces the report, watch it fail, fix the code.
It is harder when you don't yet know what you want, such as an early sketch of a page layout or an experiment with a new library. Many developers explore first, throw the experiment away, and then write the real code test-first once the shape is clear. Nobody needs to follow the loop for every line. The habit worth keeping is the one at its heart: see every test fail at least once.
How does test-first help when an AI writes the code?
Write the tests yourself, from the rule, and ask the AI tool for the code that makes them pass. Your tests then state what you want in a form the tool can't argue with, and you check its answer by running them. This works far better than asking for code and tests together, where the tool tends to write tests that agree with whatever its code does.
Watch for tools that "fix" a red test by editing the test instead of the code. If an expected value changes in a diff, ask why. In test-first work the test comes from the rule, so changing it means the rule changed, and that is a decision for a person.
Field notes
- Test first runs in a loop: a red test, a small green change, then a tidy-up.
- Seeing a test fail first proves it is able to fail at all.
- Make each green step as small as possible, one rule at a time.
- Put a test where the rule first changes the answer, such as 6 hours.
Easy to get wrong
A test that passes before the code exists hasn't tested anything. toBeDefined() is green for any number, so it never goes red and never catches a bug.
Small loops keep each failure down to one suspect. Adding untested code alongside a tested change also leaves you unsure whether the second test can fail.
The test comes from the rule. If the code disagrees, the code is what should move. Change the test only when the rule itself was misread.
Questions people ask
Is test-first the same as TDD?
Mostly. Test-driven development is the full loop of red, green and refactor, repeated in very small steps. Writing the test first is its core habit, and you can use that habit without following the loop strictly.
Doesn't writing tests first take longer?
The first time through, a little. In return you skip most of the debugging that comes from finding mistakes later, and every rule ends up with a test that you know can fail.
What if I can't think of the expected value?
That usually means the rule isn't clear yet. Ask whoever owns it: what should 1.5 hours cost? The question you could not answer is exactly the one the code would have guessed at.
Can I write several failing tests before any code?
You can, for example from a list of examples in a spec. Make them pass one at a time, and use test.todo() for the ones you haven't written yet so they show in Results without failing.