QA How-To
Playwright Python network mocking (2026)
Learn Playwright Python network mocking with route handlers, HAR replay, API fixtures, failure simulation, debugging, and interview-ready test patterns.
15 min read | 3,131 words
TL;DR
Use page.route() or browser_context.route() before navigation, inspect each Route and Request, then call fulfill(), continue_(), fallback(), or abort(). Prefer small deterministic fakes for focused UI tests, HAR replay for broader recorded traffic, and real contract tests to keep mocks honest.
Key Takeaways
- Use page.route() or browser_context.route() before navigation, inspect each Route and Request, then call fulfill(), continue_(), fallback(), or abort().
- Use observable UI state and web-first assertions instead of fixed sleeps.
- Keep test data, browser state, and artifacts isolated across workers and retries.
- Use current Python APIs directly and avoid wrappers that hide lifecycle or intent.
- Preserve actionable failure evidence while masking secrets and limiting retention.
- Validate the approach with representative CI workflows before standardizing it.
Playwright Python network mocking is most reliable when the test design makes browser lifecycle, application state, and expected evidence explicit. This guide gives working Python patterns and explains the tradeoffs a senior QA or SDET should be ready to defend.
The goal is not a clever demo. It is a maintainable approach that survives parallel CI, product change, and failure investigation. Examples use the synchronous Playwright Python API and pytest conventions that remain current in 2026.
TL;DR
Use page.route() or browser_context.route() before navigation, inspect each Route and Request, then call fulfill(), continue_(), fallback(), or abort(). Prefer small deterministic fakes for focused UI tests, HAR replay for broader recorded traffic, and real contract tests to keep mocks honest.
| Decision | Recommended default | Reconsider when |
|---|---|---|
| Scope | Smallest scope that covers the behavior | Multiple pages or shared infrastructure require coordination |
| Synchronization | Observable state and web-first assertions | Elapsed time is itself the requirement |
| Test data | Synthetic, unique, and owned by the test | A controlled shared read-only fixture is cheaper |
| Evidence | Trace plus focused failure artifacts | Privacy or storage policy requires a narrower set |
| Abstraction | Plain typed helpers around domain behavior | Repetition has not yet established a stable boundary |
1. What Playwright Python network mocking controls
Network mocking places test code between the page and an outgoing request. A handler can return a synthetic response, change request details, block a resource, or allow another matching handler to decide. This gives a UI test deterministic control over server states that are slow, rare, expensive, or destructive. The browser still executes its normal fetch and rendering logic, so the test covers more integration behavior than replacing an application service in JavaScript.
Choose the smallest interception scope that matches the assertion. page.route() affects one page, while browser_context.route() can cover every page and popup in a context. Register routes before the action that starts traffic. A route is not merely a stub declaration: every matched request must be resolved exactly once through fulfill(), continue_(), fallback(), or abort().
A useful review checklist for this area is: identify the observable requirement, choose the narrowest scope, make ownership explicit, and retain evidence when it fails. Keep setup deterministic and independent from test order. Name fixtures and helpers after product behavior so another engineer can understand the test without opening every implementation file. In CI, use the same browser and dependency versions that were validated locally, while keeping credentials outside source control.
2. Set up pytest and register a first route
Install pytest-playwright and browser binaries, then use the page fixture supplied by the plugin. Match a stable URL pattern that is narrow enough to avoid intercepting unrelated calls. Glob patterns match the entire URL, including scheme and path, so **/api/profile is often safer than a vague /api/ rule.
A handler may be synchronous in the sync API or async in the async API. Do not mix those styles inside one test suite. The example returns valid JSON, a realistic status, and an explicit content type. Registering before page.goto() prevents a startup request from escaping before the mock exists.
Treat this decision as part of the test contract. Record what the test controls, what remains real, and which failure proves the requirement is broken. This prevents a helper from silently expanding into infrastructure that nobody owns. When the product changes, update the behavioral assertion first, then adjust the supporting abstraction. That sequence keeps the suite focused on user risk instead of implementation trivia.
3. Fulfill JSON responses safely
route.fulfill() can receive status, headers, body, json, content_type, or a previously fetched APIResponse. The json argument is the clearest option for JSON-compatible Python values because Playwright serializes the value and supplies an appropriate content type. Model the production schema, including nullability and nested fields that the component actually reads.
Keep response builders near domain fixtures rather than burying large payloads in test functions. Make test intent visible by overriding only the field relevant to the scenario. If the UI depends on response headers such as pagination totals or correlation IDs, include them explicitly and assert the resulting behavior instead of assuming the body is sufficient.
A useful review checklist for this area is: identify the observable requirement, choose the narrowest scope, make ownership explicit, and retain evidence when it fails. Keep setup deterministic and independent from test order. Name fixtures and helpers after product behavior so another engineer can understand the test without opening every implementation file. In CI, use the same browser and dependency versions that were validated locally, while keeping credentials outside source control.
4. Modify, continue, and layer requests
Use route.continue_() when the current handler should send the request immediately, optionally with changed headers, method, URL, or post_data. Use route.fallback() when another matching route handler should get a chance to handle it. That distinction matters in layered test infrastructure: continue_() bypasses remaining route handlers, while fallback() passes control down the registered stack.
Header modification is useful for test-only tenant IDs or correlation values, but never use it to hide a broken authentication flow. A broad context route can add diagnostic headers, while a page-level route handles one endpoint. Keep ordering documented because matching handlers run in reverse registration order.
Treat this decision as part of the test contract. Record what the test controls, what remains real, and which failure proves the requirement is broken. This prevents a helper from silently expanding into infrastructure that nobody owns. When the product changes, update the behavioral assertion first, then adjust the supporting abstraction. That sequence keeps the suite focused on user risk instead of implementation trivia.
5. Simulate failures, latency, and edge states
route.abort() models transport failures such as a connection refusal, while fulfill(status=500) models a server that responded with an HTTP error. Those are observably different to application code. Test both when the product presents different recovery paths. For loading states, delay in the route handler only when the visual transition is itself the requirement, and keep the delay short and bounded.
A better pattern for most waiting assertions is to hold a request behind a test-controlled signal, assert the spinner, then release the response. Avoid long sleep calls, which slow the suite and still race on busy workers. Cover empty collections, malformed optional data, authorization failures, throttling, and retry success as separate behaviors with precise assertions.
A useful review checklist for this area is: identify the observable requirement, choose the narrowest scope, make ownership explicit, and retain evidence when it fails. Keep setup deterministic and independent from test order. Name fixtures and helpers after product behavior so another engineer can understand the test without opening every implementation file. In CI, use the same browser and dependency versions that were validated locally, while keeping credentials outside source control.
6. Replay traffic with HAR files
HAR replay is useful when a page calls many stable endpoints and hand-building each response would obscure the scenario. browser_context.route_from_har() can serve matching requests from an HTTP Archive. Set not_found to abort for a closed fixture or fallback when unmatched requests may reach the network. Update mode can refresh recordings deliberately, but generated HAR changes deserve review like code.
HAR is not a substitute for understanding contracts. Archives may contain tokens, personal data, timestamps, and large binary responses. Sanitize them before commit, minimize embedded content, and use URL filters when only part of the application should be replayed. A focused set of readable JSON fakes is usually easier to maintain than a broad archive.
Treat this decision as part of the test contract. Record what the test controls, what remains real, and which failure proves the requirement is broken. This prevents a helper from silently expanding into infrastructure that nobody owns. When the product changes, update the behavioral assertion first, then adjust the supporting abstraction. That sequence keeps the suite focused on user risk instead of implementation trivia.
7. Coordinate mocks with assertions
Wait for user-visible outcomes first. A response event can prove that bytes arrived, but it cannot prove that the UI interpreted them correctly. Use expect(locator).to_have_text(), to_be_visible(), or other web-first assertions after the triggering click. Add page.expect_request() or expect_response() only when request details are part of the requirement.
Place the trigger inside the expect_request or expect_response context manager so Playwright subscribes before the request begins. Assert method, selected headers, or post data without coupling to volatile query ordering. This pattern separates outbound contract evidence from rendered outcome evidence and produces failures that explain whether the page sent the wrong request or rendered the wrong result.
A useful review checklist for this area is: identify the observable requirement, choose the narrowest scope, make ownership explicit, and retain evidence when it fails. Keep setup deterministic and independent from test order. Name fixtures and helpers after product behavior so another engineer can understand the test without opening every implementation file. In CI, use the same browser and dependency versions that were validated locally, while keeping credentials outside source control.
8. Keep mocks contract-aware
A fake can pass forever after the real API changes. Reduce that risk by generating representative payloads from reviewed schemas, running service-level contract tests, and retaining a smaller set of end-to-end tests against deployed dependencies. UI mocks should test client behavior, not certify server correctness.
Treat fixture payloads as domain examples. Name them for behavior, validate them where practical, and update them in the same change as a contract revision. Do not copy a huge production response and assume realism equals quality. Small payloads expose which fields the UI truly requires and make failures easier to diagnose.
Treat this decision as part of the test contract. Record what the test controls, what remains real, and which failure proves the requirement is broken. This prevents a helper from silently expanding into infrastructure that nobody owns. When the product changes, update the behavioral assertion first, then adjust the supporting abstraction. That sequence keeps the suite focused on user risk instead of implementation trivia.
9. Debug intercepted traffic
Add temporary logging inside a route handler for request.method, request.url, and selected headers. If a handler never runs, check registration timing, full-URL glob behavior, service workers, popup scope, and whether another handler already continued the request. Network routing disables HTTP cache, which can also change observations compared with a manual session.
Use the Playwright trace viewer to connect actions, DOM snapshots, console messages, and network calls. Keep secrets out of logs and traces. When service workers own requests, configure the context with service_workers='block' for routing-focused tests, because Playwright cannot intercept every request hidden behind a service worker.
A useful review checklist for this area is: identify the observable requirement, choose the narrowest scope, make ownership explicit, and retain evidence when it fails. Keep setup deterministic and independent from test order. Name fixtures and helpers after product behavior so another engineer can understand the test without opening every implementation file. In CI, use the same browser and dependency versions that were validated locally, while keeping credentials outside source control.
10. Design a maintainable mocking layer
Centralize common route installers as plain functions or pytest fixtures that accept scenario data. Return or expose captured requests only when a test needs them. Make cleanup explicit with page.unroute() or context isolation, although a fresh context per test already prevents most leakage. Prefer one behavioral scenario per test over a universal mock server configured by many flags.
Name routes after user outcomes, such as install_cart_out_of_stock_route, rather than generic mock_api. This keeps test code readable and limits accidental reuse. The Playwright Python fixtures guide shows how to package such installers without creating hidden global state.
Treat this decision as part of the test contract. Record what the test controls, what remains real, and which failure proves the requirement is broken. This prevents a helper from silently expanding into infrastructure that nobody owns. When the product changes, update the behavioral assertion first, then adjust the supporting abstraction. That sequence keeps the suite focused on user risk instead of implementation trivia.
11. Apply Playwright Python network mocking in a complete test
A production-quality test registers the route, performs the user action, and asserts both the outgoing contract and the visible result. It also keeps unrelated traffic real or explicitly blocked. The complete example below captures a POST request and returns a deterministic accepted response.
This pattern is appropriate for focused client behavior. Complement it with the Playwright Python API testing guide and a thin real-environment smoke suite. Together they detect client regressions, API contract drift, and deployment wiring problems without making every UI test depend on a shared backend.
A useful review checklist for this area is: identify the observable requirement, choose the narrowest scope, make ownership explicit, and retain evidence when it fails. Keep setup deterministic and independent from test order. Name fixtures and helpers after product behavior so another engineer can understand the test without opening every implementation file. In CI, use the same browser and dependency versions that were validated locally, while keeping credentials outside source control.
Runnable Python example
Install dependencies with pip install pytest pytest-playwright and browser binaries with playwright install. Adapt the example URL and locators to your application.
from playwright.sync_api import Page, Route, expect
def test_submit_order_handles_accepted_response(page: Page) -> None:
captured: list[dict] = []
def handle_order(route: Route) -> None:
request = route.request
captured.append({
"method": request.method,
"body": request.post_data_json,
})
route.fulfill(
status=202,
json={"orderId": "order-42", "status": "queued"},
)
page.route("**/api/orders", handle_order)
page.goto("https://example.test/checkout")
page.get_by_role("button", name="Place order").click()
expect(page.get_by_text("Order order-42 is queued")).to_be_visible()
assert captured == [{
"method": "POST",
"body": {"sku": "QA-1", "quantity": 1},
}]
12. Production readiness checklist for Playwright Python network mocking
Before merging a routed test, review it as if the real service changed tomorrow. Identify the exact URL matcher, request methods it accepts, response schema it returns, and whether unmatched traffic is allowed. Confirm that the handler resolves every matched route exactly once. Exercise the test with the route temporarily removed so the team understands which dependency it isolates and what failure evidence changes.
Build a small scenario matrix from production behavior: success, empty result, validation error, authorization failure, server error, transport error, and recovery. Do not put every row into every component test. Select cases that drive distinct client behavior, and cover the server contract separately. Record which fields are intentionally omitted so a compact fixture is not mistaken for an accidental partial response.
For CI, prevent external traffic when isolation is a hard requirement. Log unexpected request hosts without leaking query secrets, and fail deliberately rather than allowing a test to pass against a developer service. Review HAR changes for credentials and unstable data. Add a real-environment smoke path that proves routing, authentication, and deployment still integrate.
A release-ready implementation has an owner for fixture schemas, a rule for sanitizing recordings, and a response to contract drift. Its test names describe client outcomes, not mocking mechanics. When an incident occurs, the team can tell whether the mock was inaccurate, the request was malformed, or the UI mishandled a valid response.
Finally, run linting, collection, and a representative browser test in the same container image used by CI. Review failure output with someone who did not write the test. If that person can identify the broken requirement, relevant evidence, and resource owner quickly, the implementation is ready to scale. If not, improve naming and lifecycle boundaries before adding more cases.
Interview Questions and Answers
Q: What is the difference between page.route() and browser_context.route()?
page.route() intercepts requests for one page. browser_context.route() applies across pages in that isolated context, including popups. I use page scope for local behavior and context scope for cross-page policies, then keep patterns narrow.
Q: When would you use fulfill(), continue_(), fallback(), and abort()?
fulfill() supplies a response, continue_() sends the request immediately, fallback() delegates to another matching handler, and abort() produces a network failure. The choice should represent the production condition being tested, especially the difference between an HTTP error and transport failure.
Q: How do you prevent network mocks from drifting from the API?
I keep payloads small and schema-aligned, run API or contract tests against the service, and retain a thin real-backend UI suite. Contract changes update mocks and consumers together. Mocks verify client behavior, not server correctness.
Q: Why must routes be registered before the action?
Routing is not retroactive. If navigation or a click sends the request first, the request can reach the network before the handler exists. I register during fixture setup or immediately before a controlled trigger.
Q: How is HAR replay different from hand-written mocks?
HAR replay maps recorded traffic to requests and is convenient for many stable endpoints. Hand-written mocks are more readable and scenario-focused. I sanitize archives, fail on unexpected traffic when isolation matters, and avoid treating a recording as a contract.
Q: How would you test a loading spinner without flaky sleeps?
I hold the matching route behind a test-controlled signal, trigger the action, assert the spinner, then release the response and assert the final state. That creates deterministic sequencing. A fixed sleep only guesses how long the state will last.
Q: Can Playwright intercept service worker traffic?
Routing may not see requests intercepted by a service worker. For routing tests I create the context with service_workers='block' when the application allows it. I also verify handler scope and use tracing before blaming the matcher.
Q: What should a network-mocked UI test assert?
It should assert the user outcome and, when relevant, the outbound request contract. I avoid asserting incidental traffic. A good failure tells me whether the request was wrong or the response was rendered incorrectly.
Common Mistakes
- Registering a route after navigation or after the click that starts the request.
- Calling continue_() when fallback() is required for another matching handler.
- Returning an unrealistic payload that omits headers or fields used by the UI.
- Using HTTP 500 to represent a transport failure, or abort() to represent an application error.
- Allowing HAR files to commit credentials, personal data, or unstable timestamps.
- Asserting only that a request happened instead of checking the user-visible behavior.
- Mocking every integration and then calling the result an end-to-end test.
A mature review does more than reject these patterns. It asks what pressure created them, such as slow environments, weak test data APIs, missing accessibility semantics, or insufficient failure artifacts. Fix the enabling condition as well as the individual test. Keep exceptions documented, narrow, and measurable so a temporary workaround does not become the permanent architecture.
Conclusion
Playwright Python network mocking should make tests easier to understand, isolate, and diagnose. Start with the smallest representative workflow, use the real API shown here, and verify behavior through user-visible outcomes. Keep data and artifacts safe, measure CI results, and resist abstractions that hide lifecycle or intent.
Your next step is to implement one focused scenario, review its failure evidence with the team, and then standardize only the parts that proved reusable.
Interview Questions and Answers
What is the difference between page.route() and browser_context.route()?
page.route() intercepts requests for one page. browser_context.route() applies across pages in that isolated context, including popups. I use page scope for local behavior and context scope for cross-page policies, then keep patterns narrow.
When would you use fulfill(), continue_(), fallback(), and abort()?
fulfill() supplies a response, continue_() sends the request immediately, fallback() delegates to another matching handler, and abort() produces a network failure. The choice should represent the production condition being tested, especially the difference between an HTTP error and transport failure.
How do you prevent network mocks from drifting from the API?
I keep payloads small and schema-aligned, run API or contract tests against the service, and retain a thin real-backend UI suite. Contract changes update mocks and consumers together. Mocks verify client behavior, not server correctness.
Why must routes be registered before the action?
Routing is not retroactive. If navigation or a click sends the request first, the request can reach the network before the handler exists. I register during fixture setup or immediately before a controlled trigger.
How is HAR replay different from hand-written mocks?
HAR replay maps recorded traffic to requests and is convenient for many stable endpoints. Hand-written mocks are more readable and scenario-focused. I sanitize archives, fail on unexpected traffic when isolation matters, and avoid treating a recording as a contract.
How would you test a loading spinner without flaky sleeps?
I hold the matching route behind a test-controlled signal, trigger the action, assert the spinner, then release the response and assert the final state. That creates deterministic sequencing. A fixed sleep only guesses how long the state will last.
Can Playwright intercept service worker traffic?
Routing may not see requests intercepted by a service worker. For routing tests I create the context with service_workers='block' when the application allows it. I also verify handler scope and use tracing before blaming the matcher.
What should a network-mocked UI test assert?
It should assert the user outcome and, when relevant, the outbound request contract. I avoid asserting incidental traffic. A good failure tells me whether the request was wrong or the response was rendered incorrectly.
Frequently Asked Questions
What is Playwright Python network mocking?
Use page.route() or browser_context.route() before navigation, inspect each Route and Request, then call fulfill(), continue_(), fallback(), or abort(). Prefer small deterministic fakes for focused UI tests, HAR replay for broader recorded traffic, and real contract tests to keep mocks honest.
Is Playwright Python network mocking suitable for CI?
Yes. Make browser versions, configuration, test data, and artifacts repeatable in CI. Start with a small representative workflow, measure reliability, and preserve evidence for failures.
What is the best first step for Playwright Python network mocking?
Build one focused test from the runnable example, then adapt it to a real risk in your application. Keep lifecycle ownership explicit and verify the user-visible outcome.
Should I use fixed sleep calls in Playwright Python tests?
No for normal synchronization. Use locators, web-first assertions, request or response expectations, or another observable application signal. A bounded delay is justified only when elapsed time itself is the behavior under test.
How should a team debug failures?
Keep the original Playwright error and collect a trace plus focused screenshots or logs. Reproduce with the same browser, data, and concurrency. Diagnose the first incorrect observable state instead of adding retries immediately.
How many scenarios should one UI test cover?
Usually one coherent behavior and its important outcome. Use data-driven tests only when failures remain independently diagnosable. Large journeys reduce scheduling flexibility and hide the first cause.
How do I keep the implementation maintainable?
Prefer plain typed helpers, clear fixture ownership, semantic locators, and small domain examples. Review abstractions when product behavior changes. Delete helpers that only rename the underlying API.