What goes wrong with one big test?
An expect() that fails throws an error, and that error ends the test. Every line after it is skipped. In the starting file, the fourth expect() fails on the three-job discount, and the express check on the next line never runs. Results shows one red test with one message. You fix the discount, run again, and only then meet the express bug.
With two bugs that costs you one extra round. In a real file with twenty lines in one test, it can cost many, and each round starts with reading a name like repair quotes work that tells you nothing about which rule broke.


What does one thing mean?
One behaviour, described by one sentence. It does not mean one expect() line. A test called refuses an order with no pickup day can check both the error class and the message, because together they describe that single refusal. A test that checks the price of one job, then two, then the discount, then express, is describing four rules and should be four tests.
A handy test of the idea: try to name it. If the honest name needs the word and, such as adds up jobs and gives a discount and charges express, it is doing more than one thing. If the only name that fits is vague, such as works, it probably is too.
How do you split a test without losing anything?
Take each expect() from the big test, give it its own test(), and write a name for the rule it checks. Keep the same input and the same expected value. Nothing is lost, and every check now runs whatever happens to the others.
Wrap the group in describe('quote', () => { ... }). A describe() block does not run anything itself. It groups tests under a heading, so Results shows quote › takes 5 off three or more jobs. That full name reads as a statement of the rule, and when it turns red you know where to look before opening the code.
describe('quote', () => {
test('takes 5 off three or more jobs', () => {
expect(quote(['puncture', 'brakes', 'gears'])).toBe(38);
});
test('adds 10 for express repairs', () => {
expect(quote(['brakes'], true)).toBe(25);
});
});How do focused tests help you find the bug?
After the split, two tests fail and the rest pass. That pattern is information. The single job and two-job tests pass, so adding up prices works. Three jobs gives 43 where 38 was expected, a difference of exactly the 5 discount, so the discount did not apply. Express gives 20 where 25 was expected, so express added 5 when it should add 10.
Each failure points at one branch of the code. Line 9 compares jobs.length > 3, which skips the discount for exactly three jobs. Line 13 adds 5 where the comment says 10. A big test would have given you only the first clue, and a less precise one.
Why not compute the expected value in the test?
The Break It test loops over every kind of bike and several hour counts, and works out each expected price with RATES[kind] * hours and an if for the discount. It feels thorough, twelve checks in a few lines. The trouble is that the test now contains a second copy of the rule. When that copy is written the same way as the code, it makes the same mistake. Here both say hours > 4, so the test agrees with the bug and passes.
Expected values should come from outside the code: worked out by hand from the rule, or taken from a price list someone agreed. Four hours on a mountain bike is 36, less 10%, which is 32.40. Writing that number in the test means it can disagree with the code, which is the only way it can catch anything.
When is a loop or a helper fine in a test?
Loops over hand-written data are fine when every row carries its own expected answer. That is what test.each does, and it reports each row as its own test, which a for loop inside one test does not. Prefer test.each to a loop for exactly that reason.
Helpers that build test data are fine too. A function such as freshLog() or makeBooking({ hours: 4 }) removes noise and makes each test shorter. The line to watch is a helper that decides what the right answer is. Building inputs is safe. Calculating expected outputs repeats the code.
- Fine: a helper that returns fresh sample data
- Fine:
test.eachwith an expected value in every row - Risky: an
ifthat picks whichexpect()to run - Risky: working out the expected value with the same formula as the code
Is some duplication between tests acceptable?
Yes, more than in normal code. Each test should read on its own, top to bottom, without sending you to three helpers to see what it does. Repeating quote(['brakes'], true) in two tests costs a few characters and keeps both tests clear.
Pull something into a helper when it is long, repeated many times and not the point of the test, such as building a full booking object with ten fields. Leave inline the values the test is about, so the reader sees the input and the expected answer side by side.
What do AI tools do with focused tests?
Generated tests swing between two extremes. Some tools produce one long test per function with every assertion inside it. Others produce many tiny tests that each repeat the same easy case. Watch for the first when one name covers several rules, and for the second when five tests have five names but the same input.
Look out too for expected values calculated by the test. An AI tool that has just read your code tends to reproduce its formula in the test, bugs included. If you see arithmetic on the right-hand side of a matcher, replace it with a number you worked out yourself.
Field notes
- A failed
expect()ends its test, so later checks in it never run. - One test should check one behaviour you can name in a single sentence.
- Focused tests show every failing rule together, each with a clear name.
- Expected values should be worked out by hand, never with the code's formula.
Easy to get wrong
The first failing expect() ends the test, so later checks never run. Separate tests report every broken rule at once.
Recalculating the answer in the test copies the code's logic, mistakes included. A number worked out from the rule can disagree with the code.
A loop inside one test stops at the first failure and shows one name. test.each runs and names every row separately.
Questions people ask
Is it wrong to have more than one expect in a test?
No. Several assertions are fine when they describe one behaviour, such as the class and the message of one error. Split the test when the assertions describe different rules.
Do many small tests make the suite slow?
Not noticeably. Unit tests like these take well under a millisecond each, and Vitest runs files in parallel. The cost of a test is reading and maintaining it, which focused tests make easier.
Can describe blocks be nested?
Yes. You can write describe('quote') with an inner describe('discounts'). Keep nesting shallow, two levels at most, so names stay readable in Results.
Why does the fixed Break It use toBeCloseTo?
Multiplying by 0.9 can give a result such as 64.80000000000001. toBeCloseTo() compares to two decimal places by default, which is what money needs.