skip to content

An end-to-end test for an order-detail page needs the signed-in account to already have one completed order. Why do teams create that prerequisite data with an API call or a database setup step instead of driving the application's own UI to create it first, and when is UI-driven setup still the right choice?

level: juniorimportance: should knowfreq 55%

answer

  1. setup is not the subject
  2. who gets blamed when it breaks
  3. one broken button, twenty red tests
  4. cheapest seam that is still honest
  5. cover the create flow exactly once

basics

~20 s

Creating prerequisite data through the product's API or a database setup step is faster and far less brittle, because the test then depends on no UI it is not asserting about. Drive the UI for setup only when that creation flow is itself the behaviour under test.

solid answer

~40 s

An end-to-end test has two halves: the state the scenario assumes, and the behaviour it asserts. Only the second half is what the test is for, so the first half should go through the cheapest seam that still produces honest state — normally the same HTTP API the application itself calls. Clicking through checkout to produce an order makes every page, wait and locator on that path a way for an unrelated test to fail, and the failure then names the wrong feature: an order-detail test goes red because a button moved in checkout. It is also seconds versus tens of milliseconds, multiplied by the whole suite. The exception is the creation flow itself: cover signing up, or checking out, through the real UI in one dedicated test, and seed it programmatically everywhere else.

go deeper

for a junior

Be able to say that setup data should come from an API call or a seed step rather than from clicking through the app, and give the reason: speed, plus not depending on screens the test is not about.

for a middle

Explain the seams and their prices — API seeding applies validation and side effects but couples you to a contract; direct database writes are fastest but can create states the application would never produce and leave derived data stale.

for a senior

Show how you keep failures attributable: seeding helpers that fail loudly and distinctly, one UI test per creation journey as a canary, and a rule about which scenarios are allowed to build state through the interface.

for a principal

Own the policy across teams: which seeding seam is sanctioned, who maintains the helper layer as the API evolves, and how you stop each squad from reinventing a slow UI-driven setup that quietly triples suite runtime.

## Two halves of every end-to-end test Every scenario contains a **precondition** ("an account exists that has one completed order") and an **assertion** ("the order-detail page shows the total, the items and the shipping address"). Only the assertion is the reason the test exists. The precondition is plumbing, and the seam you build it through decides how often this test fails for reasons that have nothing to do with the page it covers. ## What UI-driven setup actually costs **Failure surface.** If setup means adding an item to a cart, entering an address and confirming payment, then every page load, every element the setup clicks and every wait on that path becomes a way this test can go red. Add a confirmation step to checkout and twenty tests that were never about checkout fail. **Wrong attribution.** A failure in the setup half is a lie told by the report. The test is named for the order-detail page, but execution never reached that page. Someone has to read the trace to discover the real feature that broke — and the feature that actually broke already has its own failing test, so the extra twenty are pure noise. **Time.** A UI creation flow is several seconds of navigation, rendering and network. The equivalent API call is tens of milliseconds. Multiply by the number of tests that need the same precondition and the difference is the gap between a suite that runs on every pull request and one that runs nightly. ## The seams available, cheapest honest one first **The product's own API.** Call the same endpoints the application calls. The record passes the same validation and permission checks, and the same side effects fire — derived counters, search indexing, domain events, notifications. The state you end up in is a state the app could genuinely have reached. The cost is that your setup code now depends on that API's contract; when the contract changes, the suite breaks. That is usually acceptable, because it is a real break and someone should hear about it. ```javascript // setup: one call instead of six interactions const order = await api.createOrder(makeOrder({ status: 'completed' })); await visit(`/orders/${order.id}`); ``` **Direct database writes or a seed script.** The fastest option, and the only way to build states the API deliberately refuses to produce: an expired trial, a legacy row written by a version that no longer exists, a record dated three years ago. The price is real. You can write a row the application would never have produced and then lose a day debugging a bug you invented. And anything derived from that write — a cache, a search index, a materialised view, an event-driven projection — is not updated, so the UI may simply not show your data. **A restored snapshot or fixture dataset.** A whole known-good dataset loaded before the run. Good for read-only journeys; awkward for tests that mutate, because every mutation makes the snapshot's meaning drift. ## Cover the creation flow exactly once Seeding everything has one genuine risk: if no test ever creates data the way a user does, nobody notices when the create path breaks or when the shape it produces stops matching the shape you seed. The rule that resolves this is to cover each creation journey through the UI in **one** dedicated test — that is the test whose subject is checkout — and to seed programmatically in every other test. That single test doubles as a canary on your seeding helper: when the seeded order and the UI-created order diverge, one of the two is wrong. ## Practical rules - Make setup failures obvious and distinct from assertion failures. A seed helper that throws a clear error ("failed to create order for scenario X") stops the suite from reporting a plumbing outage as a product defect. - Seed the minimum the scenario needs. A test about the order total does not need a loyalty balance, three addresses and a saved card. - Keep the seeding helpers in one small layer with meaningful names, so the test body reads as intent rather than as HTTP. - Seeded data is still **real** data in the real system: it goes through the running backend and is stored. That is a different technique from replacing the backend's responses, and the two answer different questions.

  • Would you seed through the product's own API or write rows straight into the database?
    Prefer the API: it applies the same validation, permissions and side effects — counters, search indexing, events — so the state is one the app could really reach. Direct writes are faster and can build states the API forbids, such as an expired trial, but they can produce impossible state and leave caches or indexes stale.
  • If everything is seeded programmatically, what stops the create path from silently rotting?
    One dedicated test per creation journey that does it through the UI, exactly as a user would. That test is the subject of the create flow, and it also acts as a check that the shape your seed helper produces still matches what the application itself produces.
  • How do you keep a setup failure from being reported as a product bug?
    Give the seeding layer its own error handling: assert the seed's response, and throw a message that names the scenario and the seeding step. Then a red run reads as "could not create the order" rather than as "the order-detail page is broken", and triage takes seconds.

saying these in an interview costs you the question

  • Insists every test build its state through the UI for realism
  • Writes rows directly to the database and is surprised the app shows impossible state
  • Reuses a record an earlier test created as this test's setup
  • Thinks seeding through the API means replacing the backend
  • Treats a setup step failing as a genuine failure of the feature under test

context