How page.route() deliberately intercepts and replaces network responses, making E2E tests faster and more reliable
Playwright's page.route() API lets you intercept every single network request a page makes before it actually reaches the server, and either answer it with your own, controlled response, let the actual request pass through unchanged, or block it entirely. This capability makes route mocking one of the most effective tools against the most common cause of flaky E2E tests: dependence on a real, possibly slow or inconsistent backend system.
Table of Contents
- 1. Why real backend calls make E2E tests unstable
- 2. Basic usage of page.route()
- 3. Deliberately simulating error states and slow responses
- 4. Partial mocking: intercepting only specific requests
- 5. Differences from Cypress cy.intercept()
- 6. Common pitfalls with route mocking
- 7. When to deliberately forgo route mocking
- 8. Using HAR files for realistic, recorded responses
- 9. Mocking strategies at a glance
- 10. Summary
- 11. FAQ
1. Why real backend calls make E2E tests unstable
An E2E test that actually hits a real backend system on every run automatically inherits its entire instability: a slow database query delays the test, a temporarily overloaded third-party service makes the test fail, and constantly changing production data makes assertions against concrete content unreliable. These external factors have nothing to do with the actual quality of the frontend code under test, yet they still regularly cause failed test runs, which over time tempts developers into reflexively ignoring red tests instead of taking them seriously.
Route mocking solves this problem by decoupling frontend tests from actual backend availability: the test controls exactly what response a given network request receives, regardless of whether the real backend is currently reachable, slow, or has changed content, causing the test to check exclusively the actual frontend behavior, not the reliability of the entire infrastructure. This decoupling pays off especially in larger teams, where frontend and backend development proceed in parallel and independently of each other, since frontend tests no longer depend on an always-working, up-to-date backend state.
Another, often underestimated benefit is the considerably higher execution speed of mocked tests compared to tests with real network calls, since a locally answered, mocked request usually returns within milliseconds, while a real database round trip with network latency, application logic, and serialization can easily take ten to a hundred times as long, which adds up to substantial total runtime savings across hundreds of tests in a CI pipeline.
2. Basic usage of page.route()
The basic structure of page.route() consists of a URL pattern determining which requests get intercepted, and a handler deciding how the intercepted request gets handled, say by answering with a custom, fixed JSON response instead of actually forwarding the request to the server.
import { test, expect } from '@playwright/test';
test('shows cart count from mocked backend', async ({ page }) => {
await page.route('**/rest/V1/carts/mine', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items_count: 3, items_qty: 5 }),
});
});
await page.goto('/checkout/cart');
await expect(page.locator('[data-testid="cart-count"]')).toHaveText('3');
});
3. Deliberately simulating error states and slow responses
An especially valuable use case for route mocking is deliberately simulating error states that would be hard or impossible to reproduce against the real backend, say a 500 server error, a 429 rate-limit response, or a request that never answers, testing the frontend's timeout behavior. Without route mocking, a development team would either have to artificially push the real backend into an error state, which is risky and costly, or forgo testing these important error paths entirely.
For deliberately testing loading states and race conditions in the frontend, an artificial delay can be built into the route handler before the actual response, letting assertions about loading spinners or skeleton screens be checked reliably and reproducibly, instead of relying on randomly occurring delays in the real backend.
test('shows an error message on a server error', async ({ page }) => {
await page.route('**/rest/V1/carts/mine', (route) =>
route.fulfill({ status: 500, body: JSON.stringify({ message: 'Server error' }) })
);
await page.goto('/checkout/cart');
await expect(page.locator('[data-testid="error-banner"]')).toBeVisible();
});
test('shows a loading spinner during a slow response', async ({ page }) => {
await page.route('**/rest/V1/carts/mine', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
await route.continue();
});
await page.goto('/checkout/cart');
await expect(page.locator('[data-testid="loading-spinner"]')).toBeVisible();
});
4. Partial mocking: intercepting only specific requests
In many test scenarios, it makes more sense to mock only a single request relevant to the test and let all other requests pass through unchanged to the real backend, instead of fully isolating the entire page. This deliberate, partial mocking can be achieved by having the handler call `route.continue()` instead of `route.fulfill()` for non-relevant URL patterns, forwarding the request unchanged.
This hybrid approach is especially suited for tests that need to reflect a realistic end-to-end flow, but need to deliberately simulate a single, hard-to-reproduce state (say, "stock level at zero"), without artificially preparing the entire test environment for it.
5. Differences from Cypress cy.intercept()
Cypress offers a conceptually similar capability for intercepting and mocking network requests via cy.intercept(), but differs in one important architectural point: Cypress runs in the same browser process as the application under test and intercepts requests at the service-worker level, while Playwright implements interception via the Chrome DevTools Protocol or the respective browser debugging interfaces, giving Playwright full control across all three supported browser engines (Chromium, Firefox, WebKit) with an identical API.
In practical use, this difference means Playwright's route mocking tends to work more consistently across different browsers, while Cypress has historically been more optimized for Chromium-based browsers, an aspect that can well be a deciding factor when consciously choosing between the two frameworks.
6. Common pitfalls with route mocking
A common mistake is an overly broad URL pattern that accidentally intercepts more requests than intended, say `**/api/**` instead of a more specific pattern, causing requests irrelevant to the test but still necessary (say, loading images or fonts) to get unexpectedly blocked or corrupted, and the test failing for a completely different reason than originally intended.
Another common pitfall is letting mocked responses drift from the actual API contract shape of the real application, say through outdated or incomplete example payloads, letting a test stay green even though the real backend schema has long since changed. Regularly comparing mocked response structures against real, current API responses, say via contract testing (see the separate article on contract testing with Pact), considerably reduces this risk.
7. When to deliberately forgo route mocking
Not every test should be mocked: for the most critical, central user path of an application, say Magento's full checkout process, at least an occasional, real end-to-end test against a realistic staging environment is valuable to uncover actual integration problems between frontend and backend that pure mocking naturally can't detect.
A proven practice is therefore a mixed strategy: the vast majority of tests use route mocking for speed and stability, while a small number of critical smoke tests regularly, but less often, actually run unmocked against a real environment, to deliberately cover the "integration gap" mocking creates.
8. Using HAR files for realistic, recorded responses
Instead of manually writing every mocked response as inline JSON, Playwright supports recording real network traffic sessions as a HAR file (HTTP Archive format), which then serves as the basis for realistic mock responses drawn from actual requests, instead of using hand-written, potentially incomplete example data.
This approach combines the stability benefits of route mocking with the realism of genuine backend responses, and is especially well suited for complex API responses hard to recreate by hand, say deeply nested product catalog structures with many variants and attributes. A practical middle ground is to create a HAR recording once from a real staging environment, then check it into the test repository under version control, and selectively override individual values in it as needed, instead of producing a completely new recording for every small test case variation.
9. Mocking strategies at a glance
The table below compares the network interception approaches presented.
| Approach | Suited for | Downside |
|---|---|---|
| Inline JSON mock | Simple, fixed test cases | Manual maintenance on API changes |
| Partial mocking | Realistic flows with one special case | Still has backend dependency |
| HAR recording | Complex, realistic responses | Needs re-recording on API changes |
| No mocking (real backend) | Critical smoke tests | Slow, unstable on backend issues |
Mironsoft
E2E test strategy, CI integration, and stable test suites
Test suites that actually find bugs instead of just blinking red?
We review existing E2E test suites for flakiness, missing test isolation, and inefficient CI runtimes, then build a test strategy that genuinely creates confidence instead of just checking a box.
Test Audit
Systematically uncovering flaky tests, testing pyramid gaps, and coverage blind spots.
CI Optimization
Building parallel execution, retry strategies, and fast feedback loops.
Cypress/Playwright Setup
Setting up robust E2E suites for Magento frontends from the ground up.
10. Summary
Route Mocking: The Essentials at a Glance
Core idea
page.route() intercepts network requests before they reach the server and replaces them with controlled responses.
Strength
Error states, loading times, and race conditions can be deliberately and reproducibly simulated.
Pitfall
Overly broad URL patterns and outdated mock responses are the most common sources of error.
Balance
Mixed strategy of mocked tests for speed and a few real smoke tests for integration confidence.