Arrange-Act-Assert
The three-part shape every readable test has, why the act step should be one line, and the smells that show up when a test will not fit the pattern.
3 min read · updated 19 September 2026
Three parts, in order, separated by a blank line:
it('refuses a withdrawal that would overdraw the account', () => {
const account = new Account({ balanceCents: 5_000, overdraftCents: 0 });
const result = account.withdraw(5_001);
expect(result.ok).toBe(false);
expect(result.reason).toBe('insufficient-funds');
expect(account.balanceCents).toBe(5_000);
});Arrange — put the world in the state the behaviour needs. Act — do the one thing under test. Assert — state what should now be true.
That is the entire pattern. Its value is not that it is clever; it is that it is the same everywhere, so a stranger reading your test in six months knows where to look.
#Why the act step should be one line
If the act step is one line, the test has one subject and one reason to fail. That is the property that makes a red build informative.
When the act step grows to four lines, one of three things is true:
- The test covers two behaviours. Split it.
- Setup has leaked into act. Move it up. Creating the account is arrange, not act.
- The API requires a ritual to use. Three calls in a fixed order before anything useful happens is a design signal — see writing testable code.
The third is the valuable one. A test that will not fit into AAA is frequently telling you something about the production code, and the temptation is to blame the pattern.
#Assert on outcomes, plural where it helps
"One assertion per test" is a proxy for "one reason to fail", and the proxy is worse than the thing it stands for. Three assertions about one outcome are fine:
// C#, xUnit. One behaviour, three facets of the same result.
[Fact]
public void Refunding_an_order_restores_stock_and_records_the_credit()
{
var order = OrderBuilder.Paid(sku: "book-1", quantity: 2);
var refund = order.Refund(RefundReason.Damaged);
Assert.Equal(OrderStatus.Refunded, order.Status);
Assert.Equal(2, order.StockToRestore);
Assert.Equal(order.TotalCents, refund.AmountCents);
}What is not fine is asserting on three unrelated things, because then the first failure hides the other two and the test name cannot describe what broke.
#Keeping arrange short
Long arrange blocks are the most common readability problem in real suites. Two techniques carry most of the load.
Builders with meaningful defaults. The test names only what matters to it; everything else takes a sane default.
// Only the field under test is stated. The reader's eye goes to `tier`.
const customer = aCustomer({ tier: 'gold' });
const order = anOrder({ customer, lines: [aLine({ unitCents: 2_000, quantity: 3 })] });Named fixtures for genuinely shared setup — but only when the setup is
identical and incidental. A beforeEach that configures something the
test then asserts on has moved the arrange step out of sight, which is worse
than a long arrange block.
See test data management for the version of this problem that involves a database.
#The common failure modes
Assert inside act. expect(service.process(x)).toBe(y) collapses two
steps into one. It reads fine for trivial cases and badly for everything
else, because the thing under test is now buried inside an assertion.
Arrange after act. Usually a mock configured mid-test. It works and it reads like a mystery novel.
Act in a loop. for (const case of cases) { ... expect(...) } produces a
single test that fails with no indication of which case. Use the runner's
parameterisation instead: it.each in Jest, [Theory] in
xUnit, @ParameterizedTest in JUnit,
@pytest.mark.parametrize in pytest. Each case becomes its
own named, independently reported test.
Assert on nothing. A test that calls the code and asserts only that no exception was thrown is occasionally legitimate — and is far more often a test that somebody stopped writing halfway through.
#In end-to-end tests
The pattern holds at every level, though the arrange step moves from constructing objects to seeding state through an API:
// Playwright. Arrange via API, act via UI, assert via both.
const product = await createProduct(request, { priceCents: 1_200 });
await page.goto(`/products/${product.slug}`);
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByRole('status')).toHaveText('1 item in cart');Arranging through the UI is the single most common reason end-to-end suites are slow and flaky: it puts twelve steps' worth of risk in front of the one step you meant to test.
Common questions
- Is arrange-act-assert the same as given-when-then?
- Structurally yes, culturally no. AAA is a code-level convention for developers; given-when-then is a specification-level vocabulary aimed at being readable by people who do not read code. The same test can be described either way — the difference is who the words are for.
- Can a test have more than one act?
- It can, and it is almost always two tests wearing a trenchcoat. The exception is when the second action is part of one behaviour, such as writing a value and then reading it back to prove it round-tripped.
- Should I write the comments // Arrange // Act // Assert?
- Blank lines do the same job with less noise once the team knows the pattern. The comments are genuinely useful when teaching it, and in a language where the arrange step is unavoidably long.
Runnable samples for this page
last test results ↗- TypeScript
typescript/src/practices/arrange-act-assert
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Given-When-ThenThe Gherkin vocabulary for describing behaviour, how it maps onto arrange-act-assert, and when a shared specification language is worth its cost.
- Naming and Structuring TestsTest names that say what broke without opening the file, the naming conventions worth adopting, and how to organise a suite so people can find things.
- Unit TestingWhat a unit actually is, what belongs in a unit test and what does not, and the properties that separate a unit suite people run from one they skip.
- Writing Testable CodeThe properties that make code easy to test — pure cores, injected edges, no hidden state — and the specific smells that make it hard.
- Writing Tests in Pythonpytest fixtures, the patching rules that catch everyone, async testing, and the toolchain for a Python project that has to hold up in CI.
- Test-Driven DevelopmentRed-green-refactor, what TDD actually changes about a codebase, where it fits badly, and the honest evidence for and against it.
- NUnitThe longest-serving .NET test framework — its constraint-based assertion model, attribute set, and the shared-instance behaviour you have to work with.