Skip to content
RungsySign in

End-to-End Testing

Driving the real app like a user — high confidence, high cost, choose the flows carefully.

40 minDifficulty 3/5testingAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.

Before this

Why this exists

Every unit and integration test passes, but the deployed site's checkout button is wired to the wrong handler after a merge conflict resolution went slightly wrong, and no automated check notices because nothing actually clicked the button the way a user would. End-to-end tests drive the real, deployed application through a real browser, catching exactly this class of failure.

The mental model

An end-to-end test is a robot user: it opens a real browser, navigates to a real page, clicks real buttons, types into real fields, and asserts on what actually appears on screen — no fakes, no mocks, the full stack from the click to the database and back.

How it works

High confidence, high cost — that's the whole tradeoff

An end-to-end test exercises the entire deployed system, so passing it means the real thing genuinely works for that flow, not just each piece in isolation. But it also means every layer's latency stacks up, real infrastructure has to be running, and any layer's instability — a slow network call, a flaky third-party API — can make the test flaky too.

Choose flows by business value, not by coverage completeness

You cannot afford to end-to-end test every path through the app — the cost per test is too high. Pick the handful of flows where a failure genuinely hurts: sign-up, checkout, the core action the product exists for. Everything else is better covered by cheaper unit and integration tests.

Wait for state, don't wait a fixed amount of time

`sleep(2000)` before checking if a button appeared is both slower than necessary when things are fast, and still fails when things are slow. Waiting for a specific condition — the button existing, a network request completing — is faster on average and more reliable, because it's tied to what actually happened rather than a guess about how long it takes.

Isolate test data so runs don't interfere with each other

Two end-to-end tests running against a shared database, both creating a user named 'Test User', will collide. Each test needs its own isolated data — a fresh account, a unique email — or tests will fail intermittently for reasons that have nothing to do with the code being tested.

The mechanism

A test runner launches a real, automatable browser instance, navigates it to the deployed application's URL, and issues commands that mimic user actions — click this element, type this text. After each action, it asserts on the resulting DOM state or a visible element, using the same rendering pipeline a real user's browser would.

What people get wrong

End-to-end tests should replace unit and integration tests once you have them.
End-to-end tests are slow and give poor failure localisation — a failing checkout test tells you checkout is broken somewhere, not which of forty functions caused it, which unit tests pinpoint immediately. Relying solely on end-to-end tests means every debugging session starts from a vague symptom instead of a precise failure, even though the coverage feels comprehensive.
A flaky end-to-end test just needs a longer timeout.
Flakiness is usually caused by waiting for a fixed time instead of a specific condition, or by shared test data colliding between runs — a longer timeout treats the symptom and often just makes the flaky failure slower to occur. Papering over flakiness with longer waits accumulates into a test suite that takes much longer to run while remaining just as unreliable.
You need to end-to-end test every page and every button.
The cost per end-to-end test is high enough that comprehensive coverage this way is impractical; a small number of tests covering the highest-value flows delivers most of the confidence at a sustainable cost. Trying to cover everything at this layer produces a suite so slow and expensive to maintain that teams eventually stop running it, or stop trusting its failures.

When not to use it

You want to verify a single function's logic is correct.
A unit test — it's dramatically faster and pinpoints the exact failure, rather than requiring a full deployed environment to check pure logic.
You want to verify two internal services agree on a contract.
An integration test against those two services directly, per `unit-vs-integration` — cheaper and more precise than driving the whole UI to indirectly exercise the same interaction.

Terms

Headless browser
A real browser engine running without a visible window, used by end-to-end test tools to render pages the same way a user's browser would.
Flaky test
A test that intermittently passes or fails against unchanged code, commonly caused by timing assumptions or shared state in end-to-end suites.
Selector
A query used to locate an element on the page for a test to interact with, such as by text content, role, or a dedicated test attribute.
Test isolation
Ensuring each test's data and state don't affect or depend on any other test, so tests can run in any order or in parallel without interference.

Can you recall it?

Why do end-to-end tests give worse failure localisation than unit tests, and what does that imply about how many of them a suite should have?

Keep track of this

Add Testing to your map and Rungsy will schedule reviews so you actually remember it.