Why We're Building a 2-Line E2E Testing Framework
category: "E2E Testing"
Why We're Building a 2-Line E2E Testing Framework
The Problem Is Not Builders
What I keep seeing is not "too much builder pattern in API tests."
It is three different problems showing up at once:
- API tests are giant one-off specs with a pile of inline scenarios, repeated request code, and repeated assertions.
- Test setup gets shoved into
beforeEachhooks, helper chains, and shared fixtures until nobody can tell what a test actually needs. - UI tests get written as if the API is an afterthought, so every workflow starts by replaying the whole world through the browser.
That combination makes tests hard to debug, hard to extend, and hard to trust.
What Better Looks Like
The fix is simple, but it only works if each part has a job.
- Direct API tests prove endpoint behavior.
- Builders prepare state for higher-level tests.
- UI tests focus on the workflow, not on recreating dependencies.
That is the path to short E2E tests.
First: Stop Hand-Writing Giant API Specs
If an endpoint has create, update, list, filter, auth, validation, and boundary cases, hand-writing all of that in one spec file becomes a mess fast.
This is the kind of test file I want us to stop writing:
test('posts API', async ({ request }) => {
const create = await request.post('/api/articles', {
data: {
article: {
title: 'Hello',
description: 'Short',
body: 'Long form content',
},
},
});
expect(create.status()).toBe(201);
const duplicateTitle = await request.post('/api/articles', {
data: {
article: {
title: 'Hello',
description: 'Again',
body: 'More content',
},
},
});
expect(duplicateTitle.status()).toBe(409);
const emptyTitle = await request.post('/api/articles', {
data: {
article: {
title: '',
description: 'Bad data',
body: 'Still bad',
},
},
});
expect(emptyTitle.status()).toBe(400);
// ...and 40 more scenarios inline
});The problem there is not line count alone. It is that transport, data, expectations, and intent are all mixed together.
The API Pattern We Actually Want
For direct API coverage, the spec should mostly orchestrate.
The reusable parts live in four places:
PostUtils.tsfor request methodsPostValidations.tsfor domain assertionspost.types.tsfor typed payloads and responsesPost.jsonandPostNegative.jsonfor scenarios
That gives us specs that look like this:
const cases = baseSetup.getTestCases(JSON_FILE, 'tc_createPostTest');
for (const tc of cases) {
test(`[${tc.useCaseId}] ${tc.description}`, async () => {
const response = await postUtils.createPost(tc.request);
postValidations.validateCreateResponse(softAssert, response, tc.expected);
softAssert.assertAll();
});
}Now the test tells you what failed, the JSON tells you which scenario failed, and the validation layer tells you which field failed.
That is debuggable.
Second: Builders Fix Setup Sprawl
The next problem is not direct API coverage. It is setup.
People usually solve setup like this:
test.beforeEach(async ({ request }) => {
user = await createUser(request);
auth = await login(request, user);
tag = await createTag(request, auth);
article = await createArticle(request, auth, tag);
});That works until the fourth or fifth dependency. Then every test starts with invisible setup and shared state.
Builders are the answer here, but specifically for seeding:
const { user, article } = await testData.post()
.withAuthor((author) => author.withUsername('alice'))
.withTag('testing')
.create();That is not replacing direct API tests. It is removing setup noise from higher-level tests.
Third: UI Tests Should Arrive Late
If lower-level API behavior has already been covered directly, the UI test can do less.
That is the point.
test('reader can favorite a post', async ({ page, testData }) => {
const { article, user } = await testData.post().withAllDeps().create();
await login(page, user);
await page.goto(`/article/${article.slug}`);
await page.getByRole('button', { name: /favorite/i }).click();
await expect(page.getByText('1')).toBeVisible();
});The UI test is short because the API work was handled on purpose, earlier.
The Through-Line for This Series
This series is really about three moves:
- Make API tests data-driven so they are broad and debuggable.
- Use builders to localize and simplify setup for higher-level tests.
- Keep UI tests small by arriving with state already prepared.
That is how you get to two-line tests without pretending the hard parts do not exist.
What Comes Next
The order matters.
- We start by setting up Playwright for direct API and UI work.
- Then we build the data and cleanup primitives.
- Then we move into data-driven API coverage.
- Only after that do we build fluent seeding and short UI tests.
If you skip the API-first part, the short tests are fake.
Next: Installing Playwright and Building the API-First Foundation ->