Fixtures, setup and test data
A test needs a known starting point: the app running, maybe a logged-in user, and — crucially — data in a known state. If tests share data and step on each other, you get the flakiness the automation-strategy module warned about: a test that passes alone and fails in the suite. This lesson is how Playwright's fixtures give each test a clean setup, and how to handle test data so tests stay independent and reliable — the discipline that separates a suite you trust from one you do not.
Fixtures: setup handed to your test
A fixture is a piece of set-up (and tear-down) that Playwright prepares and hands to a test. You have
used one already: page. When you write async ({ page }) => …, Playwright creates a fresh browser context
and page for that test and cleans it up afterwards. Each test gets its own page — isolated from the
others — which is why tests do not leak browser state into each other.
You can also do your own setup in hooks:
test.beforeEachruns before every test in a file — the place for common setup.test.afterEachruns after — for clean-up.test.beforeAll/afterAllrun once for the file.
AgentPay's transactions tests use beforeEach to reach a known screen before each test:
test.beforeEach(async ({ page, request }) => {
await resetData(request); // clean, known data (below)
await loginAs(page, ADMIN.email, ADMIN.password);
await page.getByRole('link', { name: 'Transactions' }).click();
await expect(page.getByRole('heading', { name: 'Transactions' })).toBeVisible();
await expect(page.getByRole('row')).toHaveCount(11); // wait for the first page to load
});
Every test in that file therefore begins from the same, known place — no copy-pasted setup, no test assuming a previous one left things a certain way.
The heart of it: controlling test data
The most important reliability question in UI automation is: does each test control the data it depends on? A test that asserts "the table shows 60 transactions" is only reliable if the data is known to be those 60. If another test creates a transaction, the count becomes 61 and the first test fails — for no real bug. This is the shared-state flakiness from the flaky-tests lesson, and it is extremely common.
AgentPay solves it directly. The API keeps its data in memory and can reseed to a fixed baseline, exposed (only when a test flag is set) as a reset endpoint. The test helper calls it:
export async function resetData(request: APIRequestContext) {
await request.post('http://localhost:4000/__test/reset');
}
and every beforeEach calls resetData first. So every test starts from the identical, deterministic
seed — the same 60 transactions, the same agents — no matter what any other test did. The "create
transaction" tests add rows, but the reset before the next test wipes them. This is why AgentPay's suite is
reliable: no test depends on another, because the data is reset to a known state before each one.
Notice the trade-off it forced: because all tests share one API server and reset it, they must run
serially (AgentPay sets workers: 1). That is a deliberate choice — a small, reliable serial suite over
a fast, flaky parallel one, exactly the value the automation-strategy module argued for. A bigger project
would instead give each worker its own isolated data (a database per worker, or per-test unique records) to
keep parallelism; the principle is the same either way: each test owns its data.
Strategies for independent data
You will meet these approaches to test data, in rough order of preference:
- Reset to a known seed (AgentPay) — reseed a controllable test backend before each test. Simple, deterministic; needs a test-only reset hook and usually serial runs against a shared instance.
- Create what you need, per test — each test creates its own records (via the API in setup) and does not assume anything pre-exists. Scales to parallel runs because tests do not collide. AgentPay's create tests lean this way (they make their own transaction).
- Unique data per test — when tests must share a backend, use unique values (a unique email, a timestamped id) so they cannot clash.
- Never depend on data a human left in a shared environment, or on tests running in a particular order — that is the classic route to a flaky suite.
Arrange through the API, assert through the UI. A fast, reliable pattern: set up state with API calls
(create the records you need, log in) and use the browser only for the behaviour you are actually testing.
Driving the UI to create setup data is slow and brittle; a request.post is quick and sturdy. AgentPay
does this — it resets and logs in via the API layer, then tests the screen.
Check your work
Fixtures are set-up/tear-down Playwright hands to a test; page is one (each test gets a fresh,
isolated page). Hooks: beforeEach/afterEach (per test), beforeAll/afterAll (per file). AgentPay's
beforeEach reaches a known screen before each test.
Control the data — the key reliability question. A test asserting a count is reliable only if the data
is known; another test changing it causes shared-state flakiness. AgentPay resets the API to a fixed seed
before each test (resetData → a guarded reset endpoint), so every test starts identical. That forces
serial runs (workers: 1) — a deliberate reliable-over-fast choice.
Independence strategies: reset to a known seed; create-what-you-need per test (scales to parallel);
unique data per test on a shared backend; never depend on human-left data or test order. Arrange via the
API, assert via the UI — set up state with fast request calls, drive the browser only for the behaviour
under test.
Practice
- Add a
beforeEachto a spec that logs in and navigates to a known screen; confirm each test starts there. - Explain how AgentPay's
resetDatakeeps tests independent, and what would break without it. - Write a test that arranges its state through the API (
request.post) and asserts through the UI. - Take two tests that share data and make them independent, either by resetting or by each creating its own.
- Explain the trade-off AgentPay made by resetting a shared API (serial runs) and when you would instead isolate data per worker.
- Give an example of order-dependent tests and how you would remove the dependency.
Official documentation
- Playwright — Fixtures — Built-in and custom fixtures, and isolation.
- Playwright — Test hooks (
beforeEachetc.) — Setup and teardown hooks. - Playwright — API testing /
request— Arranging state through API calls.
Next: traces, screenshots and debugging a failure.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship