The CRUD tests in Chapter 13 all
shared a rhythm: create an article, do something, delete it at the end. That
arrange-and-clean-up boilerplate multiplies across a suite. This chapter extracts it
into reusable provisioning — and closes Part 3.
Code for this chapter is tagged
ch-14in the repo:
https://github.com/aktibaba/playwright-qa-course — seesrc/utils/scenarios.ts
andsrc/fixtures/scenarios.fixture.ts.
Two layers: a pure helper and a fixture
Split provisioning into two pieces, because they have different jobs:
1. A pure util — knows how to create the resource, nothing about test
lifecycle. Reusable from anywhere (a fixture, globalSetup, another helper):
// src/utils/scenarios.ts
export async function createArticle(
api: APIRequestContext,
overrides: ArticleInput = {},
): Promise<Article> {
const res = await api.post("articles", {
data: {
article: {
title: overrides.title ?? uniqueTitle(),
description: overrides.description ?? "Seeded by a scenario helper",
body: overrides.body ?? "Body text.",
tagList: overrides.tagList ?? [], // always send it (Ch.13 quirk)
},
},
});
if (!res.ok()) throw new Error(`createArticle failed: HTTP ${res.status()}`);
return (await res.json()).article as Article;
}
2. A factory fixture — wraps the util with lifecycle. It hands the test a
function, remembers everything that function creates, and deletes all of it on
teardown:
// src/fixtures/scenarios.fixture.ts
export const test = authTest.extend<ScenarioFixtures>({
makeArticle: async ({ authedApi }, use) => {
const created: string[] = [];
const make = async (overrides: ArticleInput = {}) => {
const article = await createArticle(authedApi, overrides);
created.push(article.slug);
return article;
};
await use(make);
for (const slug of created) {
await authedApi.delete(`articles/${slug}`).catch(() => {}); // teardown
}
},
});
A fixture that provides a function instead of a value is the key move: the test
can call it as many times as it likes, and cleanup scales automatically.
Tests start from the state they need
Now a test that needs an existing article gets one in a line — and never cleans up:
test("a provisioned article is retrievable by slug", async ({ makeArticle, api }) => {
const article = await makeArticle({ title: "Findable Article", tagList: ["scenario"] });
const res = await api.get(`articles/${article.slug}`);
const found = (await res.json()).article;
expect(found.title).toBe(article.title);
expect(found.tagList).toEqual(["scenario"]);
});
test("a provisioned article appears in the global feed", async ({ makeArticle, api }) => {
const article = await makeArticle();
const res = await api.get("articles");
const slugs = (await res.json()).articles.map((a: { slug: string }) => a.slug);
expect(slugs).toContain(article.slug);
});
No try/finally, no tracked slugs, no afterEach. The fixture owns it. Compare with
the manual await authedApi.delete(...) we wrote at the end of every CRUD test —
that's exactly the boilerplate that's now gone.
Why the split matters
Keeping the util separate from the fixture pays off repeatedly:
- The util is callable outside a test —
globalSetup, a CLI seed script, or another fixture can all reusecreateArticle. - The fixture is the only thing that knows about per-test lifecycle, so cleanup policy lives in exactly one place.
- It composes: a future
makeArticleWithCommentsfixture builds oncreateArticleplus a comments helper, and still tears everything down at the end.
This is the shape real frameworks use for test-data provisioning.
Part 3, done
You can now test the API as a first-class surface: read assertions, authenticated
sessions, full CRUD, and reusable provisioning with automatic teardown. Together
with Part 2's architecture, that's the API milestone complete.
Next up — Part 4: Integration
This is the differentiator. Chapter 15 — Auth once with storageState: log in a
single time, save the browser session to disk, and start UI tests already
authenticated — no logging in through the form on every test. Tag: ch-15.
Following along? Star the repo
and tell me what your most-used test-data factory provisions.
Top comments (0)