End-to-End Testing: A Guide to Both Layers
What end-to-end testing means, how it differs from unit and integration tests, why E2E suites get slow and flaky, and what to run in the browser vs the API.

End-to-end testing checks a whole user journey through the real system: log in, add to cart, pay, and confirm the order, across the interface, the services and the database. You can run that journey in two places. A browser test clicks through the UI. An API flow test sends the same requests the UI would send, with no browser. Both are end-to-end. They cost different amounts and break for different reasons. On one shop journey, 10 runs of a browser test took 11.2 s and 10 runs of the API flow took 2.2 s. This guide covers what E2E means, where it sits next to unit and integration tests, why E2E suites get slow and flaky, and what to keep in the browser.
What end-to-end testing means
An end-to-end test runs a journey the way a user would, from the first request to the last response. It passes only if every part along the way works together. Rerun after each change, that journey also doubles as a regression check, as covered in regression testing vs unit testing.
That is the difference from smaller tests. A unit test fakes everything around one function. An integration test connects a few parts. An end-to-end test connects all of them: the interface or the API, every service the journey touches, the database, and the network between them.
A good E2E test follows a real journey, such as:
- sign up, confirm the email, and log in;
- search, add two items to the cart, and check out;
- create a project, invite a teammate, and change their role.
Each journey crosses many parts of the app. That is the value: an E2E test catches the bug that only shows up when everything runs together. It is also the cost, since there are more places for the test to go wrong.
End-to-end vs integration vs unit tests
Unit tests check one piece. Integration tests check a few pieces together. End-to-end tests check a whole journey.
| Unit | Integration | End-to-end | |
|---|---|---|---|
| Scope | One function or class | A few connected parts | A full user journey |
| Runs against | Fakes and stubs | Real neighbors, or stand-ins | The running app and its services |
| Typical count | Many | Fewer | Fewest |
| Fails because | The logic changed | A connection broke | Anything along the journey broke |
Google’s testing blog suggests a 70/20/10 split, 70% unit, 20% integration and 10% end-to-end, as a good first guess. The exact numbers matter less than the shape: many small tests at the bottom, few large ones at the top.
The line between integration and end-to-end blurs at the API. An API flow that logs in, creates a record and updates it is an integration test of your app. It is also an end-to-end test of that journey, minus the screen, and Integration Testing Examples for a REST API shows four such tests in full. The comparison of integration testing vs end-to-end testing puts the top two layers side by side. The end-to-end testing tools guide compares the tools for each layer. The guide to unit tests vs integration tests covers the lower two layers in detail. In that experiment, one API flow test caught 6 of 8 bugs and a 2,189-test unit suite caught 5.
Two layers of end-to-end: browser and API
An end-to-end journey can enter your app in two places. A browser test enters through the UI. An API flow test enters at the HTTP layer, below the UI.
Browser tests drive a real browser. They type into forms, click buttons, wait for pages and check what the user sees. Playwright is the strongest tool here. It runs Chromium, WebKit and Firefox on Windows, Linux and macOS, and it emulates mobile browsers. A browser test is the only test that sees your layout, your client-side logic and your accessibility.
API flow tests send the requests your UI would send, in the same order. Log in and keep the token. Create a category and keep its ID. Create a product in that category, then update it. Each step reads values from the previous response and checks the status and the body. There is no page to load, so there is nothing to wait for but the server.
Both layers test the same journey through the same backend. They differ in what they can see, and in what can break them.
| Browser test | API flow test | |
|---|---|---|
| Drives | A real browser | HTTP requests |
| Sees | The UI and everything behind it | The API and everything behind it |
| Can’t see | API response fields the page never shows | Layout, client-side logic, accessibility |
| Breaks on UI-only changes | Can, depending on its selectors | No |
Why end-to-end suites get slow and flaky
E2E suites get slow because each test starts a lot of machinery. They get flaky because each test depends on a lot of moving parts. Most of the trouble comes from four places.
A flaky test is one that passes or fails on different runs with no change to the code. Concurrency, timing and reliance on outside systems are the usual causes.
Timing. A browser test waits for the page to be ready. A fixed sleep waits too long or not long enough. Playwright handles most of this for you: its assertions retry until the condition is met, which removes a whole class of flakes. Animations, slow third-party scripts and network jitter still add variance.
Selectors. A browser test has to find elements on the page. If it finds them by CSS class or DOM structure, a harmless markup change breaks it. Playwright’s best practices say to avoid relying on implementation details such as the CSS class of an element, and to prefer user-facing attributes.
Test data. A journey that creates a user named “test” fails the second time if the name must be unique. Each run needs fresh data, or a reset between runs.
Shared state. Tests that share a login, a cart or a database row fail in a different order. Playwright recommends that each test run fully isolated, with its own storage, data and cookies.
API flow tests share the last two problems. They need fresh data and isolated state too. They skip the first two: there is no page to wait for and no selector to find.
What we measured on one shop journey
We ran the same journey both ways on a small shop app: log in, add a category, create a product, change its price. The UI test used Playwright. The flow test was recorded with Stresseur. These numbers come from that one app, not a general benchmark.
The setup: a 2-vCPU, 7 GB Linux container sized like a CI runner, with the app and the tests in the same container. We timed each full command, including browser or CLI start.
Speed. 10 runs of the UI test took 11.2 s in total. 10 runs of the flow took 2.2 s.
Stability. Both passed 50 of 50 runs. This app is too simple to flake, so we don’t claim the flow flakes less.
A UI-only change. We renamed the Edit button’s CSS class, two lines of front-end code, with the API untouched. The UI test failed after the 30 s test timeout, and the flow passed all 8 steps. To be fair to the UI test: it found that button by its CSS class. That is the pattern Playwright’s docs advise against. A role-based locator would not depend on the class name.
A real change. Then we made the API require a SKU on new products, which broke both tests. The same coding agent, with the same model and prompt, fixed each test 3 times from a fresh copy. The median time to green was 21 s for the flow and 61 s for the UI test. The flow’s failure named the step and the server’s answer: PostProducts … got 400: Product name, price and sku are required. The UI test’s failure said element(s) not found. Both fixes were small.
So the flow test was faster to run, harder to break for UI-only changes, and quicker to fix on this journey. The browser test is still the only one that would catch a broken Edit button.
How to write your first end-to-end test
Pick one journey, give it its own data, and assert on the outcome a user cares about. Start small and make it pass every time before you add a second.
- Pick the journey that would hurt most if it broke. Usually sign-up, login or checkout.
- Write down the steps and the end state. “Log in, add one item, pay, and see the order with status paid.”
- Give the test its own data. Create a fresh user or a unique name on each run, so a second run doesn’t collide with the first.
- Assert on the outcome. Check the order’s status and total. A page that loaded proves little.
- Run it ten times in a row. If it fails even once with no code change, fix the cause before you trust it.
Then decide which layer it belongs in. If the journey’s risk is in the screen, write it as a browser test. If the risk is in the logic behind the screen, write it as an API flow, and keep one browser smoke test on top.
What to move below the browser, and what to keep in it
Move a check to the API layer when the browser adds nothing to it. Keep it in the browser when the user’s screen is the thing you are testing. Our guide to API automation testing covers which API checks to automate first.
Move these to API flows:
- Business journeys. Checkout math, permissions, state changes across several requests.
- Edge cases. Bad input, missing fields, limits, expired tokens. Each one is a few lines of YAML and a fraction of a second.
- Checks that run many times. Anything you want on every pull request, or after every change your coding agent makes.
Keep these in the browser:
- Layout. Does the button show, is the form usable on a phone.
- Client-side logic. Validation, state and routing that never reach the API.
- Cross-browser behavior and accessibility.
- Third-party widgets that run in the page, such as an embedded payment form.
A common split is a small set of browser smoke tests, one per critical journey, plus API flows for the journeys and their edge cases. The multi-step API flows guide shows how tokens and IDs pass between steps.
Finding and fixing flaky end-to-end tests
Find flaky tests by running them more than once, then fix the cause, not the symptom. A retry hides a flake; it doesn’t remove it.
Find them. Rerun failed tests on the same commit. A test that fails and then passes with no change is flaky by definition. Track which tests do this over a week, and fix the worst ones first.
Quarantine, briefly. Move a known flaky test out of the required check while you fix it, so the rest of the suite stays trusted. Give it an owner and a date, or it stays quarantined forever.
Fix by cause. Replace fixed sleeps with waits on a real condition. Replace CSS selectors with user-facing locators. Give each test its own data. Stop tests from sharing logins and rows. The causes behind flaky tests, and which checks to move below the browser, get their own guide.
Move it down a layer. If a browser test keeps flaking on a check that doesn’t need the screen, such as a price calculation or a permission rule, move that check into an API flow and keep the browser test for the UI.
Running end-to-end tests on every pull request
Give your E2E suite a time budget per pull request, and fit the layers to it. A suite that takes too long gets skipped, moved to nightly, or made optional.
Fast API flows fit on every pull request. A handful of browser smoke tests usually fits too. Run the full browser suite where it pays off, such as before a release or on a schedule. Whatever runs on the pull request, make it a required check so a red result blocks the merge. The workflow file is in running API tests on every pull request.
When a test does fail, the message decides how long the fix takes. “Element not found” sends you to the page. “Step PostProducts got 400: sku is required” sends you to the cause.
End-to-end testing tools
For the browser layer, Playwright and Cypress are the common choices. Playwright covers Chromium, WebKit and Firefox. For the API layer, you can write flows by hand, use a collection runner, or record them from real traffic.
Playwright can also send requests straight to your REST API, with no browser. That makes it a good option if you want one tool for both layers and you’re happy writing the API steps by hand. Stresseur records how your API is used during development and turns those journeys into YAML flows that run on every pull request. For a side-by-side view, see API end-to-end tests vs browser tests. For API tools in general, see API testing tools compared and the API testing guide.
End-to-end tests prove that a whole journey works. Run that journey in two layers. Keep a small set of browser tests for what users see, and write them with user-facing locators so markup changes don’t break them. Move the journey’s logic and its edge cases to API flows that run on every pull request. Each layer catches bugs the other can’t.
Frequently asked questions
What is unit testing vs end-to-end testing?
A unit test checks one function or class on its own, with its neighbors faked. An end-to-end test runs a whole user journey through the real system, from the entry point to the database and back. Unit tests are many and cheap; end-to-end tests are few and cover the paths users take.
What does E2E testing mean?
E2E is short for end-to-end. An E2E test checks a complete user journey, such as sign up, log in and check out, across every part of the system it touches: the interface or API, the services, the database and the network between them.
What is a flaky test?
A flaky test passes or fails on different runs without any change to the code. Common causes are timing, shared state between tests, and calls to systems outside your control.
Can Playwright test APIs too?
Yes. Playwright can send requests straight to your app's REST API, without a browser. Many teams use it for both layers. The choice is less about the tool and more about which layer each test belongs in.