Arrange, act, assert
The shape is not a rule for its own sake: it makes the test scannable. A reader finds the setup, the one call being tested, and the claim — without reading the whole body. Tests that interleave the three are the ones nobody dares change.
it("charges the agreed price, not the catalogue price", () => {
// arrange
const door = { id: 2, price: 890 };
const line = { doorId: 2, qty: 3, unitPrice: 820 };
// act
const total = lineTotal(line, [door]);
// assert
expect(total).toBe(2460);
});
One reason to fail
The first version tells you "works — failed". The second tells you which behaviour broke, from the name alone, in the CI summary, without opening the file.
Several expects in one test are fine when they describe one behaviour — a returned object checked field by field. The test is about one claim, not about one assertion.
it("works", () => {
expect(total(a)).toBe(1780);
expect(total(b)).toBe(0);
expect(() => total(null)).toThrow();
expect(format(total(a))).toBe("1,780");
});
it("multiplies quantity by price", () => { … });
it("is zero for an empty order", () => { … });
it("refuses a missing order", () => { … });
it("formats the total with a thousands separator", () => { … });
The name is the documentation
Read describe and it together as a sentence: "lineTotal › is zero when the order has no lines". If that sentence is not a fact about the system, the test is testing the wrong thing.
| Instead of | Write |
|---|---|
it("total") | it("multiplies quantity by the agreed price") |
it("works with empty") | it("is zero when the order has no lines") |
it("test 3") | it("refuses a quantity of zero") |
it("should handle errors") | it("throws when the door is not in the catalogue") |
Fixtures: build them, do not share them
// shared, and one test mutating it breaks the next
const ORDER = { id: 1, lines: [{ qty: 2, price: 890 }] };
// fresh every time, and the overrides say what this test cares about
const makeOrder = (overrides = {}) => ({
id: 1,
lines: [{ qty: 2, price: 890 }],
status: "quoted",
...overrides,
});
it("cancels a paid order", () => {
const order = makeOrder({ status: "paid" }); // the one thing that matters is visible
…
});
A test that builds an order with fifteen fields hides which one the behaviour depends on. A factory with sensible defaults, plus an override for the field under test, makes every test self-explaining.
The things that make a test hard to trust
- Logic in the test. An
ifor a loop computing the expected value means you have implemented the thing twice, and both can be wrong the same way. Write the answer as a literal. - Assertions in a hook.
afterEachchecking something hides which test failed. - Tests that depend on order, or on the previous test’s leftovers.
- A comment explaining what the test does. Rename the test instead.