What are you building in this project?
You are writing the test suite for the Crumb and Crust checkout. The code in price.js already works: it adds up a basket, takes 10% off orders of 30 or more, adds VAT at 20% on the discounted price, and rounds everything to the penny. It also refuses nonsense such as a negative price or a quantity of zero.
A suite for working code sounds pointless until you think about next month. Someone will add a loyalty discount, or change VAT handling, or ask an AI tool to tidy the rounding. Your tests are what tells them, within a second, whether the old rules still hold. That is why Check my work doesn't look at your test names or count your lines. It breaks the code in four realistic ways and asks whether your suite notices each one.
Everything from the earlier lessons comes together here: matchers, edge cases, error tests, comparing objects, one idea per test, and names that read as rules.
Where do you start with a file you didn't write?
Start with the rules, not the code. The comments in price.js list them: discount at 30 or more, VAT after discount, round to the penny, whole quantities of 1 or more. Write each one down as a test name before writing any test body. A list of names is a plan you can check against, and test.todo('gives 10% off an order of exactly 30') lets you keep that plan in the file while you work.
Then group the names by function with describe(). Four groups, subtotal, discount, vat and checkout, plus one for lineTotal validation, make the Results view read like a short specification. When something fails later, the group tells you which part of the checkout to open.
describe('discount', () => {
test.todo('gives 10% off an order of exactly 30');
test.todo('gives nothing off an order of 29.99');
});Which numbers should the tests use?
Numbers you can check in your head. A basket of two cakes at 15 gives a subtotal of 30, a discount of 3, a net price of 27, VAT of 5.40 and a total of 32.40. Every step is easy to verify on paper, and it has to be, because the expected value has to come from the rules and never from running the code and copying the answer.
Pick values that sit on the edges of each rule. The discount rule changes at 30, so test 30 and 29.99. The validation rule changes at 1, so test 0. A test at 45 or 100 tells you the discount works somewhere, and almost nothing about whether the boundary is in the right place. The broken copy that uses > in place of >= passes any test that stays away from 30.
How do you test the order of two calculations?
Charging VAT before or after a discount gives different totals, and both look plausible. On the 30 basket, VAT after discount is 5.40 and the total is 32.40. VAT before discount is 6.00 and the total is 33.00. A test that only checks the discount, or only checks the subtotal, can't tell the two apart.
The cleanest check compares the whole result of checkout() with toEqual(). If any one of the four numbers is wrong, the Results view shows Expected and Received side by side and the wrong field stands out. This is one of the few places where a single test checking an object with four fields is the right choice, because those four numbers are one answer to one question.
test('charges VAT on the price after the discount', () => {
const basket = [{ name: 'Celebration cake', price: 15, qty: 2 }];
expect(checkout(basket)).toEqual({
subtotal: 30, discount: 3, vat: 5.4, total: 32.4,
});
});

How do you catch a rounding bug?
VAT at 20% often lands between two pennies. A bun at 2.99 carries 0.598 of VAT, which should round to 0.60. Code that rounds down with Math.floor charges 0.59, a penny short on every bun. Floating point adds a second trap: 1.4 * 0.2 gives 0.27999999999999997, because decimal fractions can't be stored exactly in binary, so rounding down turns a clean 28 pence into 27.
To catch a rounding bug you need an input whose raw result falls between pennies. vat(2.99) does. Neat inputs, like vat(10) or vat(1.15), land on a whole penny and pass with either kind of rounding, which is why a suite full of round numbers can miss this bug completely. When you aren't sure an input exercises the rounding, console.log(2.99 * 0.2) in a test and read the Output view.
Use toBe() on rounded results and toBeCloseTo() on raw ones. Once round() has done its job the value should be exactly 0.6, and a test that allows near misses would hide the very bug you are looking for.
How should the error tests look?
Wrap the call in an arrow function so expect() can run it and watch for the throw: expect(() => lineTotal({ name: 'Bagel', price: 1.2, qty: 0 })).toThrow('Quantity'). Without the wrapper, the error escapes before expect() sees it and the test fails with the raw error, which looks like a bug in the code.
Pass part of the message to toThrow(). A bare toThrow() passes for any error at all, including a TypeError from a typo inside lineTotal(). Matching 'Quantity' proves that the right rule rejected the input. Test the zero case, because 0 is the boundary, and add a negative price for the other rule.
How does Check my work grade a suite?
It runs your tests against the real code first, and they all have to pass. Then it runs them again against four broken copies of price.js: one with the discount boundary moved, one that charges VAT before the discount, one that rounds down, and one that accepts a quantity of zero. For each copy, at least one of your tests has to fail. If none does, the message tells you which kind of test is missing.
Your names, your numbers and the number of tests are up to you. A suite that catches all four with six tests is better than one that misses one with twenty. That is the measure professional teams care about too, under the name mutation testing: a suite is as good as the bugs it would catch.
Open the Coverage view as you go. It shades every line your tests reached. Full shading is a good sign and still no proof: the subtotal test reaches round(), yet only the vat(2.99) test would notice it rounding the wrong way.
What does an AI-written suite for this file usually miss?
Ask an AI tool to test price.js and you often get a tidy file that passes and catches less than you would expect. The common gaps are the ones this project targets. Discount tests use 50 or 100, never 30. The checkout test asserts toBeDefined() or checks only total with a value worked out by running the code. Error tests use bare toThrow(). Rounding gets no test, because the tool picked round numbers.
Review generated tests the way Check my work does. For each rule in the code, ask which test would fail if that rule broke, and try it: change >= to > yourself and watch the Results view. If everything stays green, the suite has a hole there.
- Every rule has a test on its boundary
- Expected values come from the rules, worked out by hand
- Object results are compared in full
- Error tests name the error
- At least one input exercises the rounding
Field notes
- Start from the written rules and turn each one into a test name.
- Test the discount at exactly 30 and just under it, at 29.99.
- Compare the whole
checkout()result withtoEqualto check VAT order. - Use an input like
vat(2.99)whose raw result falls between pennies.
Easy to get wrong
A test far from the boundary passes whether the rule says > or >=. Bugs in rules cluster at the edges, so put your inputs there.
A vague assertion survives almost any bug. Work out the exact answer on paper and compare it in full.
Round inputs give round answers, so they can't tell rounding up from rounding down. Pick an input whose raw result falls between two pennies.
Questions people ask
Should I test the private `round()` function directly?
No. It isn't exported, and it doesn't need to be. Test it through vat() and checkout(), the way the rest of the site uses it. If the rounding breaks, those tests fail.
Why does Check my work accept tests with names different from mine?
It judges what your tests do, never how they are worded. Any suite that passes on the real code and fails on each broken copy is a good suite.
Is one test with four fields in `toEqual()` too much for one test?
Here it is one answer: the four numbers on a receipt. If the receipt is wrong, one test fails and the diff shows which number. Separate behaviours, like validation, still get separate tests.
Would this suite run unchanged in Vitest or Jest?
Yes. Save it as price.test.js next to price.js and run npx vitest or npx jest. In Jest you would drop the vitest import, because Jest provides the globals itself.