Arrange, Act, Assert — the shape of every good test
Nearly every well-written test, in any language or framework, breaks into the same three phases — and most test-readability problems trace back to those phases getting blurred together instead of kept visually distinct.
3 min read
The three phases, made explicit
test("removing an item updates the cart total", () => {
// Arrange — set up the exact starting state this test needs
const cart = new Cart();
cart.addItem({ id: "sku-1", price: 25 });
cart.addItem({ id: "sku-2", price: 40 });
// Act — perform the one action actually being tested
cart.removeItem("sku-1");
// Assert — check the one outcome that action should have produced
expect(cart.total).toBe(40);
});Arrange builds the exact starting conditions the test needs — nothing more, nothing borrowed from elsewhere in the file. Act performs the single action under test, ideally one line. Assert checks the result. This isn't a framework feature or special syntax — it's a naming convention for a shape good tests already tend to have, and writing it out (even just as comments, like above) makes it much easier to spot a test that's actually doing something more complicated than it should be.
What it looks like when the phases blur together
// Hard to read — arrange, act, and assert are interleaved throughout
test("cart stuff", () => {
const cart = new Cart();
cart.addItem({ id: "sku-1", price: 25 });
expect(cart.total).toBe(25);
cart.addItem({ id: "sku-2", price: 40 });
expect(cart.total).toBe(65);
cart.removeItem("sku-1");
expect(cart.total).toBe(40);
cart.applyDiscount(10);
expect(cart.total).toBe(36);
});This test isn't wrong exactly — every assertion in it is checking something real — but it's testing four different behaviors (adding, adding again, removing, discounting) in one test with one vague name, which means a failure at the third expect doesn't clearly say "removing an item is broken"; it just says "something in this long chain broke," and whoever's debugging it has to re-read the whole test to figure out which behavior actually failed. Splitting this into four small, clearly-named tests — one behavior each, one clean Arrange/Act/Assert each — makes every individual failure immediately diagnosable.
One logical action, even if it's more than one line
test("submitting a valid form clears all fields and shows a success message", () => {
// Arrange
render(<ContactForm />);
fillIn("name", "Alice");
fillIn("email", "alice@example.com");
// Act — these two lines together ARE the one action: "submit the form"
const submitButton = screen.getByRole("button", { name: "Submit" });
fireEvent.click(submitButton);
// Assert
expect(screen.getByText("Message sent!")).toBeInTheDocument();
expect(screen.getByLabelText("Name")).toHaveValue("");
});"One action" in the Act phase doesn't mean literally one line of code — it means one logical action from the system's point of view. Finding a button and clicking it are two lines that together represent a single user action ("submit the form"); asserting on both the success message and the cleared field in the Assert phase is fine too, since both are outcomes of that same one action, not two different actions being tested at once.
Why this pattern generalizes across every testing tool
Every mainstream testing framework — Jest, pytest, JUnit, Go's testing package, RSpec — supports this shape natively, because it isn't tied to any framework's API; it's just "set up the world, do the thing, check what happened," which is close to how people naturally describe verifying anything, in or out of software. Recognizing the three phases in someone else's test, even in an unfamiliar framework or language, is usually enough to understand what that test is actually checking without reading any documentation first.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What are the three phases of the AAA (Arrange, Act, Assert) pattern?
2. What's the actual problem with a test that interleaves several arrange/act/assert cycles for different behaviors?
3. Does the Act phase have to be exactly one line of code?