Arrange, act, assert: the shape of a good test
Every clear test follows the same three-part shape. Learn to spot it, write it on purpose, and see why messy tests almost always skip a step.

Here's a test nobody wants to debug at 5 p.m. on a Friday:
it("discounts work", () => {
const cart = { items: [{ price: 1000 }, { price: 2000 }] };
expect(applyDiscount(cart, "SAVE10").total).toBe(2700);
cart.items.push({ price: 500 });
expect(applyDiscount(cart, "SAVE10").total).toBe(3150);
expect(applyDiscount({ items: [] }, "SAVE10").total).toBe(0);
});Three different scenarios, tangled into one test with a name that describes none of them. When this fails, the error tells you a line number, not which scenario broke. You'll spend more time figuring out what failed than fixing why. The fix isn't cleverness. It's a shape that's been around since long before Vitest existed: arrange, act, assert.
The three parts
Arrange. Set up everything the test needs: build the input, seed the data, create the object under test. Nothing here should look surprising when you read the test later.
Act. Run the one thing you're actually testing. Ideally, one line.
Assert. Check that what happened matches what you expected. This is where expect() lives.
Rewritten:
One scenario per test. One reason to fail. The test's name, "applies a 10% discount to the subtotal", tells you exactly what broke before you've read a single line of the failure output.
Why splitting scenarios matters more than it looks
The empty-cart case and the three-item case from the original example aren't extra assertions bolted onto the same test. They're different scenarios, and each deserves its own it():
Now if the empty-cart case breaks tomorrow, you get a failure that says "empty cart totals zero", not a stack trace pointing at line 4 of a 20-line test you wrote three months ago. That specificity is the entire payoff of arrange-act-assert. The failure message does your debugging for you, and you never have to open the file to find out which scenario broke.
Naming is part of the test
A good it() description reads as a sentence when you glue it to describe(): describe("applyDiscount") plus it("applies no discount for an unknown code") reads as "applyDiscount applies no discount for an unknown code." If you can't write that sentence cleanly, the test is probably checking more than one thing, or checking the wrong thing entirely.
Compare it("test 3") or it("works"), both of which show up constantly in real codebases and tell you nothing when they go red in a CI log at 2 a.m. Future-you, or whoever's on call, deserves better than a title you'd have to click through to understand.
Arrange doesn't need to be long
For a pure function like applyDiscount, arrange might be one line, or zero if the input is a literal in the act step. The pattern still applies. It's about keeping the three concerns separate, not about padding every test with setup code it doesn't need.
It scales past unit tests
The pattern isn't specific to pure functions like applyDiscount. Later in this series, when we test a real Express API with supertest, the shape is identical: arrange means registering a user and getting a token, act means firing the HTTP request you actually care about, and assert means checking the status code and body. The syntax changes. The shape doesn't. Once you can spot arrange-act-assert in a ten-line unit test, you'll spot it in a fifty-line integration test too, and you'll notice immediately when someone's buried three requests worth of "act" into what should have been one.
What this doesn't fix
Arrange-act-assert won't save you from testing the wrong behavior, and it won't catch a test that's technically well-shaped but proves nothing useful. Structure is necessary, not sufficient. The next lesson tackles a different problem: writing the test before the code that makes it pass, which forces you to think about what "correct" means before you can talk yourself into a shortcut.
Catch up on the last lesson, your first unit test with Vitest, if you skipped it. Otherwise, on to TDD: red, green, refactor.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


