Give each part of a test one job
A test is easier to understand when it answers three questions in order: What state did we start with? What did we do? What should have happened? Arrange, act, assert is a name for that sequence. Arrange creates the inputs and any required dependencies. Act calls the behavior under test. Assert checks the result.
The pattern does not require comments or blank lines in every test. Its value is that a reader can find the important action quickly. If a test prepares data, calls several unrelated functions, and checks many outcomes, a failure can be hard to interpret. Prefer a test with one main behavior and expectations that directly describe that behavior. A test may still need more than one assertion when those assertions describe the same outcome.
Start with a test you can run
Here is a small example for Node.js using ECMAScript modules, the built-in node:test runner, and node:assert/strict. Use a Node.js environment that provides those modules. Save the following function in subtotal.mjs. Prices are integer cents so this example stays focused on test structure rather than decimal currency arithmetic.
export function subtotalCents(items) { return items.reduce( (sum, item) => sum + item.unitCents * item.quantity, 0 ); }
Save the test in subtotal.test.mjs in the same directory. Run it with node --test. The input is arranged first, the function is called once, and the expected total is asserted last.
import test from 'node:test'; import assert from 'node:assert/strict'; import { subtotalCents } from './subtotal.mjs'; test('adds the extended price of each item', () => { const items = [ { unitCents: 250, quantity: 2 }, { unitCents: 125, quantity: 1 } ]; const total = subtotalCents(items); assert.equal(total, 625); });
Use fixtures to remove noise, not meaning
A fixture is prepared test data. For a short test, an object written directly in the test is often the clearest fixture: the reader can see every relevant value without looking elsewhere. Once several tests need the same ordinary item shape, a small factory can remove repetition. Add this helper near the tests that use it:
function makeItem(overrides = {}) { return { unitCents: 250, quantity: 1, ...overrides }; }
A test for an empty order needs no factory. A test for multiple quantities could arrange const items = [makeItem({ quantity: 2 }), makeItem({ unitCents: 125 })]; then act with const total = subtotalCents(items); and assert assert.equal(total, 625);. Each call creates a fresh object, so one test does not need to edit a shared item for another test.
Keep the helper deliberately boring. Its defaults should describe a valid, unsurprising item, while overrides make the values important to a particular test visible. A fixture that builds an entire checkout system to test a two-field calculation adds more to inspect than it saves. If a reader must trace several helpers to discover why an assertion expects 625, the fixture is too indirect.
Make failures point to a specific behavior
Test names should say what the behavior does, not restate the function name. For example, 'returns zero for an empty list' communicates more than 'tests subtotalCents'. Add a separate test for that case: arrange const items = []; act with const total = subtotalCents(items); and assert assert.equal(total, 0);. When it fails, the name and expected value together identify the intended behavior.
Choose assertions that check observable results. Avoid calculating the expected value with the same formula used by the production function; repeating a mistake in both places can make a test pass incorrectly. For small examples, a literal expected total is often easier to review. Also avoid claiming coverage the tests do not provide. The function above assumes items contain valid integer-cent prices and quantities; these tests do not establish how invalid inputs should be handled.
Keep setup local as tests grow
As a suite grows, it can be tempting to move every input into shared setup. First ask whether a reader would understand the test faster with the data beside the action. Local setup makes differences between cases apparent and reduces the chance that changing a shared fixture alters unrelated tests. Extract a factory when it removes repeated incidental fields, not merely because two tests use objects.
The same structure works for asynchronous code. Arrange any required inputs, act by awaiting the operation, then assert its resolved value or rejection. If the test changes global state, installs a mock, or creates a temporary resource, restore or remove it so later tests do not depend on execution order. Arrange, act, assert is a guide to clarity, not a limit on necessary cleanup.



