Playwright Network Mocking — Mock Response Shape Mismatch
Frontend renders no data; console shows 'undefined' errors because mock response keys don't match TypeScript interfaces.
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
- page.route() intercepts HTTP requests before they leave the browser.
- route.fulfill() returns a fake response with custom status, headers, and body.
- route.continue() lets the request proceed, optionally modifying headers or payload.
- route.abort() blocks unwanted requests (trackers, images, fonts).
- Mocking eliminates backend flakiness — tests become deterministic and fast.
- Performance: aborting heavy assets can cut test execution time by up to 40%.
Think of network mocking like a stunt double for your backend. Instead of your browser talking to the real server, Playwright replaces the conversation with a pretend server that returns exactly what you want. This lets you test your UI in any scenario — success, failure, slow network — without waiting for the real server to be ready.
Testing a modern frontend often feels like a hostage situation — you are at the mercy of the backend's availability and speed. Playwright's network interception capabilities break this dependency.
By leveraging the route API, you can turn your browser automation into a powerful mocking engine. Whether you need to simulate a 500 Internal Server Error, mock a slow 3G connection for performance testing, or entirely replace a JSON response to test a specific UI state, Playwright handles it natively via the DevTools protocol. This guide moves beyond basic automation into advanced network manipulation strategies for production-grade test suites.
How Playwright Network Mocking Actually Works
Playwright network mocking lets you intercept and override HTTP responses at the browser level, without touching your backend. The core mechanic is — you define a URL pattern and provide a custom response body, status, or headers. Playwright's Python API gives you full control over request interception, blocking, or modification before the browser processes the response.page.route()
What matters in practice: Playwright mocks at the network layer, not the application layer. When you mock an API call, the browser receives a synthetic response exactly as if the server sent it. This means your frontend code runs unchanged — no test doubles, no environment variables. You can simulate any HTTP status, delay, or malformed payload. The key property is that the mock response must match the shape your frontend expects; a mismatch causes silent failures or cryptic errors in your application logic.
Use this when you need deterministic, fast tests that isolate frontend behavior from backend availability, latency, or data variability. It's essential for testing error states, edge cases (empty arrays, missing fields), and race conditions. In real systems, teams rely on it to validate UI rendering without spinning up staging environments, cutting test execution time from minutes to seconds.
The Core Concept — page.route()
In Selenium, mocking network traffic usually required a separate proxy like BrowserMob. In Playwright, you simply define a routing rule. When the browser makes a request that matches your pattern (URL or Glob), Playwright intercepts it and gives you control over the route object.
route.fulfill(): Provide a mock response (status, headers, body).route.continue(): Let the request proceed to the internet (possibly with modifications).route.abort(): Kill the request (useful for blocking ads or analytics).
- Patterns are checked in order of registration
- First match wins, so define specific routes before generic fallbacks
- You can chain multiple handlers by calling
route.fallback()in advanced scenarios
page.goto().route.fulfill() with a realistic JSON body.route.continue() with modified headers/body.route.abort() — no response sent.Simulating Edge Cases: API Failures and Timeouts
One of the hardest things to test is how your UI behaves when the server dies. Using Playwright, you can force a 500 error or even a total connection failure to ensure your error handling actually works.
route.abort() simulates a dropped connection.route.fulfill() with the appropriate status code.route.fulfill() with a very long delay or call route.abort() after a timeout.route.fulfill() with an empty body or invalid JSON.Advanced: Modifying Outgoing Requests
Sometimes you don't want to mock the whole response, but you need to inject a specific header (like an Auth token) or modify a payload before it leaves the browser. Use for this.route.continue()
route.continue_() works, but some headers (Content-Length, Host) are read-only. Playwright will warn if you try to override them.Performance Testing: Mocking Slow Networks
Playwright's browser_context lets you simulate network conditions like offline mode or specific throughput limits — useful for testing how your app degrades gracefully.
Asserting Network Activity: Testing That Requests Were Made
Sometimes you don't need to mock a request — you just need to verify that a specific API call was made with certain parameters. Playwright provides and page.wait_for_request() for this. These are critical for confirming analytics events, logging calls, or form submissions actually fired correctly.page.wait_for_response()
You can combine these with route interception to validate both the outgoing request and the incoming response.
page.wait_for_request() for most test scenarios.route.abort() is too aggressive, the request never reaches the mock handler.page.expect_request() to capture the outgoing request.page.expect_response() to capture the response (mocked or real).The Python Mock Library: Why You’re Already Using It Wrong
Mocking isn’t about faking — it’s about controlling the uncontrollable. Your payment gateway, your weather API, your auth provider — any external service that can fail at 3 AM on a Sunday. The Python mock library gives you surgical control over those dependencies so your tests don’t explode when Stripe has an outage.
The standard library’s unittest.mock provides two core classes: Mock and MagicMock. The difference? MagicMock pre-implements magic methods like __len__ and __iter__. Use Mock for plain objects, MagicMock when you need dunder methods. That’s it. Don’t overthink it.
The real power comes from patch(). It’s your scalpel for temporarily replacing real objects with mocks. Use it as a decorator for whole functions, or as a context manager when you need surgical precision. Each gives you a fresh mock that won’t leak between tests.
requests.get globally will break every other test running in your suite. Patch at the module boundary — your own code that wraps the external call.Managing Side Effects: Realistic Failure Simulation Without the Pain
A mock with a static return value is a toy. A mock with side effects is a weapon. The side_effect attribute is how you simulate real-world chaos — timeouts, rate limits, intermittent failures, and conditional responses.
Pass an iterable to side_effect and each call pops the next value. Pass a callable and get dynamic behavior based on input arguments. Raise an exception when you need to test your error handling path. This is how you prove your circuit breakers actually break, your retries actually retry, and your logging actually logs.
The alternative — hoping your staging environment reproduces these scenarios — is how production incidents get shipped. You don’t need a chaos monkey. You need side_effect.
Pro tip: chain exceptions and return values in a single list to simulate flaky services. First call raises, second returns success. Your code handles both, or you find the bug before it finds you.
side_effect = lambda args: success_response if args['id'] == 42 else error_response for request-dependent mocking.Understanding Lazy Attributes and Methods
Lazy attributes are properties or methods that aren't evaluated until accessed. In Playwright network mocking, this becomes a trap when you mock a route handler that references lazy objects like request headers or response bodies. If your mock checks request.post_data in a conditional but the handler lazily evaluates it during assertion, you get inconsistent results. For example, page.route('**/api', lambda route: route.fulfill(body=json.dumps({'fake': True}))) works fine, but if you lazily fetch body from an external source, the mock may return stale or partial data. Always compute lazy attributes inside the route handler before passing them to assertions. A common pattern: capture request.url and request.headers into local variables on arrival. This ensures that by the time you inspect them in your test assert statements, they reflect the exact state of the request at interception time, not some deferred evaluation.
Common Mocking Problems: Interface Changes and Misspellings
A frequent error in Playwright network mocking is misspelling the route pattern or method name. For instance, page.route('/api', handler) instead of '*/api' will silently match nothing. Worse, if the test expects a request to be made but the mock never fires, assertions on route.activity will pass falsely. Another problem: when the object interface changes — e.g., Playwright updates request.post_data to request.post_data_buffer — old mocks break. To avoid this, use specifications: define a clear dict of expected request properties (url, method, headers) and compare them against captured data using strict assertions. For example, expected = {'url': '/api/login', 'method': 'POST'} then assert captured == expected. This catches both misspellings and interface drifts. Also, always validate that the route was actually executed by checking a counter or flag inside the handler, not just relying on side effects.
Mock Response Shape Mismatch
- Always align mock response structure exactly with the API contract.
- Use TypeScript types or OpenAPI specs as the source of truth for mock shapes.
- Add a test that validates mock response against the real API schema — catch mismatches early.
r.abort()) before any navigation.context.route() to apply a route handler to all pages in the same browser context.page.route('**/api/users', lambda r: r.fulfill(status=200, body='[]'))context.tracing.start(snapshots=True, screenshots=True, sources=True)Key takeaways
page.route for fine-grained control over individual HTTP requests based on URL patterns.expect_request to verify that the right data was sent.Common mistakes to avoid
4 patternsUsing hardcoded full URL instead of glob pattern
Forgetting to register routes before page navigation
page.route() before page.goto() or before the action that triggers the request. Routes are attached per-page and only intercept future requests.Mocking a response with incorrect Content-Type
route.fulfill() to match the expected MIME type, e.g., content_type='application/json'. Use the browser's network panel to copy the original response headers.Overusing route.continue() without proper handling
route.continue() when you need to modify the request. For most test scenarios, route.fulfill() or route.abort() is sufficient. Reduce the glob pattern scope to avoid intercepting every resource.Interview Questions on This Topic
How does Playwright's network interception differ from using a library like 'Responses' or 'HTTPretty' in Python?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
That's Python Libraries. Mark it forged?
5 min read · try the examples if you haven't