skip to content

How do you decide how much of a Playwright suite's setup runs over HTTP instead of through the UI?

level: principalimportance: should knowfreq 28%

answer

  1. Speed against coverage
  2. Seeded steps go unexercised
  3. One journey test per flow
  4. Whose session holds the seed
  5. Seed on shipped endpoints only

basics

~20 s

Default to HTTP for preconditions: it is fast and stops a test breaking on screens it is not about. Keep one test per journey that still does it as a user, and seed onto the session the browser will use.

solid answer

~50 s

Preconditions should normally go over HTTP with `page.request` or a context from `playwright.request.newContext()`: it is faster, it fails as an error rather than a confusing assertion, and it stops a test about the dashboard from breaking when the sign-up form changes. The cost is real, though — every step you seed is a step no longer exercised, so each user journey needs at least one test that still performs it through the UI. Two rules keep this honest. First, seeded state must land on the session the browser will use, or the test goes green against the wrong principal; use `page.request` when it must be the signed-in user. Second, seeding couples the suite to endpoints, so the endpoints used for setup should be product API, not private paths that exist only for tests. Teardown deserves the same discipline.

code

typescript · 14 lines
typescript
import { test, expect } from '@playwright/test';

// Bulk preconditions over HTTP; the add-a-city journey has its own UI test.
test('the dashboard paginates a long favourites list', async ({ page }) => {
  for (const city of ['Bergen', 'Oslo', 'Tromso', 'Bodo']) {
    const saved = await page.request.post('/api/favourites', { data: { city } });
    await expect(saved).toBeOK();
  }

  await page.goto('/dashboard');
  await expect(page.getByRole('listitem')).toHaveCount(3);
  await page.getByRole('button', { name: 'Next page' }).click();
  await expect(page.getByRole('listitem')).toHaveCount(1);
});

go deeper

for a junior

Understand that setting up data with an HTTP call is faster than clicking through it, and that the steps you skip are steps the test no longer checks.

for a middle

Explain the tradeoff concretely: which surface to seed with, why a shared precondition belongs in a hook, and how a seeded record ends up invisible to the browser.

for a senior

Decide per suite where the line sits, protect one journey test per flow, and keep seeding on shipped endpoints so setup does not diverge from real behaviour.

for a principal

Own the policy and its review triggers, including whether test-only routes are ever acceptable and how the team notices coverage quietly disappearing behind convenient seeding.

Every browser test needs the world to be in some state before it starts. Playwright lets you arrange that state two ways — by driving the UI, or by calling the server directly through an `APIRequestContext`. Choosing between them is a suite-level decision, not a per-test whim, and it is worth writing down. ## The default, and why Arrange preconditions over HTTP unless there is a reason not to. Three reasons carry most of the weight: - **Cost.** A `POST` is one round trip. The same setup through the UI is navigation, rendering, several actions and their auto-waiting, repeated in every test that needs it. - **Blast radius.** If the sign-up form changes, UI setup breaks every test that used sign-up as a precondition. Those failures point at the wrong feature and cost a morning to triage. - **Failure semantics.** A seeding call that fails is an error in a hook, reported as setup broken. The same failure buried in a test's opening steps looks like the feature under test is broken. ## What each seeded step costs The cost is coverage, and it is easy to forget because nothing turns red. If every test seeds its favourite cities over HTTP, no test ever proves that the button that adds one works. The discipline is to keep the journey covered exactly once: 1. One test drives the real path end to end — the user adds a city through the interface. 2. Every other test that merely needs a city to exist seeds it over HTTP. That gives one guardian per journey and cheap setup everywhere else. When someone deletes the guardian because it is slow, the coverage disappears silently, so it is worth naming those tests so their purpose is obvious. ## The session question This is the failure mode specific to Playwright. A seeded record must belong to the identity the browser will act as. `page.request` and `context.request` share the browser context's cookie jar, so a call made through them is the signed-in user. The `request` fixture and any context from `playwright.request.newContext()` are isolated, and unless you gave them `storageState` or an auth header they are somebody else — often nobody. The symptom is a test that seeds successfully, navigates, and finds an empty screen, with every status in the 2xx range. Decide once which surface seeding uses, and make it visible in the helper's name. ## Coupling and ownership | Question | Setup over HTTP | Setup through the UI | |---|---|---| | Speed | One round trip | Full navigation and interaction | | Breaks when | The endpoint's contract changes | Any screen on the path changes | | Proves the user path | No | Yes | | Failure reads as | Broken setup | Broken feature | | Couples the suite to | Server contracts | Screen structure | Prefer endpoints the product actually ships. A backdoor route that exists only for the suite drifts from real behaviour, can seed states the product cannot reach, and becomes a security question the moment it is deployed anywhere real. If setup genuinely cannot be expressed through the product's own API, that is a signal about the API, not a licence to add a private route. ## How to draw the line - Seed **bulk and background** data over HTTP: the twenty rows a pagination test needs, the account that must exist, the reference data. - Drive **the behaviour under test** through the UI, always. - Keep **one journey test per user-visible flow** that performs the flow for real. - Prefer `page.request` when the state must belong to the signed-in user; use an isolated context deliberately when it must not. - Tear down the same way you set up, and make teardown independent of the test having passed. ## Signals the line is in the wrong place Too much UI setup shows up as long runtimes, and as one form change turning a wide band of the suite red. Too much HTTP setup is quieter: coverage gaps nobody notices, tests that pass while the feature is broken in the browser, and a growing pile of seeding helpers that encode more product rules than the product does. Review the balance when either symptom appears rather than on a schedule.

  • How do you keep HTTP seeding from silently eroding coverage of a user flow?
    Name and protect one test per flow that performs it through the interface, and treat it as the reason the seeding helper is allowed to exist. Reviewing new seeding helpers for a matching journey test keeps the pair together; without that, the guardian gets deleted for being slow and nobody notices what left with it.
  • Should a Playwright suite get test-only endpoints to make seeding easier?
    Prefer the product's own API. A test-only route drifts from real behaviour, can create states the product cannot, and is a deployment and security question wherever it exists. If setup cannot be expressed through the shipped API, treat that as feedback about the API rather than as a reason to add a private path.

saying these in an interview costs you the question

  • Seeds every flow over HTTP and tests none through the UI
  • Drives sign-up through the interface in every test
  • Adds test-only endpoints to make seeding convenient
  • Ignores which session the seeded state belongs to
  • Leaves teardown to run only when a test passes