August 1, 2026
Playwright vs Selenium for Network Interception, API Mocking, and Contract-Style UI Tests
A practical comparison of Playwright vs Selenium network interception for API mocking, fixture setup, and contract-style UI tests, including tradeoffs, edge cases, and when simpler managed alternatives fit better.
When a UI test depends on backend state, the test is no longer just about buttons and selectors. It is also about controlling HTTP traffic, shaping responses, and making sure the front end behaves correctly when the backend is slow, missing, partial, or intentionally mocked. That is where the discussion around Playwright vs Selenium becomes much more concrete than a generic end-to-end testing comparison.
The practical question is not which tool can click a button. Both can. The question is which stack makes it easier to intercept requests, stub data, isolate a UI flow from a live dependency, and keep those tests understandable after the first implementation sprint.
If your tests depend on deterministic backend responses, the real cost is usually not the first mock, it is the tenth revision of that mock when the API shape or fixture setup changes.
What network interception is actually doing in a UI test
Network interception in browser automation usually means one of three things:
- Observe traffic, log requests and responses without changing them.
- Modify traffic, rewrite headers, query parameters, bodies, or status codes.
- Fulfill traffic, short-circuit a request and return a mocked response from the test.
For UI tests, the third category matters most. It lets a test force the application into a known state without requiring a seeded database, a live third-party service, or a fragile shared staging environment.
This is useful for cases such as:
- pricing or plan pages that depend on account tier
- checkout flows that depend on payment provider responses
- dashboards that need a specific set of records
- error handling screens, retries, empty states, and permission denials
- contract-style UI tests that verify the UI reacts to specific API shapes
The tradeoff is clear: the more the test controls the network, the less it resembles production integration. That is not automatically bad, but it does mean the test is validating UI behavior against a contract you defined, not the full live backend stack.
Short answer: Playwright is the more direct fit
For Playwright vs Selenium network interception, Playwright is the stronger default because network interception is built into the framework. You can route requests, fulfill them, and bind mocks to test lifecycle hooks with relatively little ceremony.
Selenium can participate in the same overall testing strategy, but it does not provide native browser-network mocking in the same way. Teams usually compensate with one of these patterns:
- inject JavaScript into the page and monkey-patch
fetchorXMLHttpRequest - run a proxy such as BrowserMob Proxy or a custom MITM layer
- stand up test doubles at the backend/API layer and point the UI at them
- shift the mocking logic into the application under test, which is usually a design smell for production code
That means the Selenium path is possible, but typically more indirect and more operationally expensive.
Playwright: request interception is part of the core model
Playwright’s route API lets tests intercept browser requests before they leave the page context, which is the cleanest place to do UI-level request control. In practice, this makes it easier to express test intent close to the user journey.
A minimal example:
import { test, expect } from '@playwright/test';
test('shows empty orders state', async ({ page }) => {
await page.route('**/api/orders', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ orders: [] }),
});
});
await page.goto(‘https://example.app/orders’); await expect(page.getByText(‘No orders yet’)).toBeVisible(); });
That example is small, but it shows the main advantage. The fixture lives in the test, the response is explicit, and the assertion can be written against a deterministic state.
Why this is practical
- Per-test control: you can mock one route for one scenario and leave other calls live.
- Readable intent:
route.fulfill()says exactly what happened. - Scoped behavior: mocks can be registered in a test or
beforeEach, which keeps suite-wide side effects limited. - Async-friendly: Playwright’s execution model and auto-waiting reduce the amount of homegrown synchronization code around requests.
Common failure modes in Playwright interception
Playwright is not magic. The errors usually come from design, not syntax.
- Over-mocking: if every dependency is mocked, the test stops checking whether the app still integrates with the real backend contract.
- Route matcher drift: a URL pattern that worked for
/api/orderscan miss/api/orders?sort=descif the matcher is too narrow. - Hidden app retries: if the application retries requests, a mock that fulfills once may not model the sequence the UI actually triggers.
- Race conditions in setup: the route must be installed before the page triggers the request, otherwise the real backend may be hit unexpectedly.
A practical rule is to treat intercepted responses as test fixtures with ownership. If the API contract changes, the fixture should change through the same review path as the UI assertions.
Selenium: possible, but interception is an externalized concern
Selenium was designed as a browser automation API, not as a network virtualization layer. That distinction matters. You can still use Selenium effectively for UI flows that depend on backend responses, but the response control usually happens outside Selenium itself.
The common patterns are below.
1. Mock at the backend boundary
Instead of intercepting browser traffic directly, point the application to a test backend, stub service, or sandbox environment. This can be stable and clean if the application supports configuration for base URLs, feature flags, and environment endpoints.
Example in Python with a backend pointing to a test server, then verifying the UI:
from selenium import webdriver
from selenium.webdriver.common.by import By
options = webdriver.ChromeOptions() driver = webdriver.Chrome(options=options) try: driver.get(‘https://staging.example.app/orders?backend=test’) assert ‘No orders yet’ in driver.find_element(By.TAG_NAME, ‘body’).text finally: driver.quit()
This looks simple, but the complexity has moved elsewhere. You now need a controllable backend, a deployable test double, and rules for how data is seeded and reset.
2. Intercept through the browser context with JavaScript
Some teams inject JavaScript before app scripts run and override fetch or XMLHttpRequest. That can work, but it is brittle and easier to break with application refactors.
A sketch of the idea, not a recommendation:
script = """
window.originalFetch = window.fetch;
window.fetch = async (url, options) => {
if (String(url).includes('/api/orders')) {
return new Response(JSON.stringify({ orders: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
return window.originalFetch(url, options);
};
"""
driver.execute_cdp_cmd(‘Page.addScriptToEvaluateOnNewDocument’, {‘source’: script})
This approach depends on Chrome DevTools Protocol support and browser-specific plumbing. It is not a universal Selenium feature, and it adds a maintenance surface that is easy to underestimate.
3. Proxy the traffic
A proxy can inspect and rewrite requests regardless of browser automation library. This is powerful in large test rigs, but it introduces infrastructure, certificate handling, and a separate failure domain. For teams already struggling with flakiness, that proxy becomes one more moving part to debug.
API mocking in browser tests, what should be mocked and what should not
API mocking in browser tests is most useful when the test needs to validate the UI’s response to a specific backend condition, not when it needs to re-run the backend’s own logic.
A useful split is:
- Mock volatile or hard-to-control dependencies, such as payment processors, email services, search indexes, or third-party identity providers.
- Leave core domain logic real when the point of the test is to validate integration across the app, database, and API.
- Mock response shape, not implementation details, unless you are deliberately testing a specific edge case.
For example, if the UI shows a discount badge when account.plan === 'pro', a mock with a minimal JSON response is appropriate. If the UI calculates billing totals based on many server-side rules, a lightweight mock may hide too much behavior. In that case, a more integrated environment may be the better choice.
A test becomes more valuable when the mocked response is the smallest thing that still lets the UI prove the behavior under test.
Fixture setup, where teams often pay the real cost
The phrase “just mock the API” hides a lot of engineering work. The difficulty is not writing the first stub, it is managing the fixture lifecycle.
Playwright fixture setup
Playwright gives you several sensible places to centralize request control:
test.beforeEachfor scenario-wide setup- custom fixtures for repeated routes and seeded data
- helper functions for domain-specific mock builders
- parallel-safe test files with isolated data per worker
A pattern that scales better than ad hoc route stubs is to define response factories:
function ordersResponse(overrides = {}) {
return {
orders: [],
...overrides,
};
}
Then build test scenarios from those factories. The benefit is not code reuse alone, it is consistency. When the API changes, you update one builder rather than many inline JSON blobs.
Selenium fixture setup
With Selenium, fixture management is often externalized to one of these layers:
- environment configuration
- a seeded test database
- contract stubs running as separate services
- page-injected mocks
That separation is fine if the organization already has disciplined environment orchestration. The risk is that the test suite becomes dependent on scripts, containers, and setup steps that are not visible from the test code itself. New contributors then have to understand the browser automation plus the surrounding infrastructure before they can debug a failure.
Contract-style UI tests are not full contract tests
The phrase “contract-style UI tests” is useful, but it should be interpreted carefully.
These tests usually verify that the UI behaves correctly when the backend returns a response of a known shape. They are not the same as formal consumer-driven contract testing between services. They are closer to an acceptance test with a stubbed backend contract.
That distinction matters because the failure modes differ:
- UI contract-style tests catch rendering issues, missing fields, disabled states, and broken flows.
- Service contract tests catch schema drift and consumer/provider mismatches earlier in the delivery chain.
The best use of UI contract-style tests is to protect visible behavior that is expensive or awkward to reproduce through full-stack integration every time. For example:
- a table should render partial data without crashing
- a banner should appear when an API returns 403
- a retry button should show after a 500 response
- a modal should prefill fields from a specific response payload
Example in Playwright
typescript
await page.route('**/api/profile', route =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ name: 'Ava', role: 'admin' }),
})
);
await page.goto(‘/settings’);
await expect(page.getByText('Admin access')).toBeVisible();
This is useful because the test expresses the UI contract directly. If the UI changes to look for userRole instead of role, the test fails in a way that is easy to interpret.
Decision criteria, not winner-takes-all
The right choice depends on what your team is trying to optimize.
Choose Playwright when you need
- native request interception in browser tests
- concise test-local mocking for many scenarios
- fast iteration on UI flows that depend on backend data
- a single codebase that covers browser actions plus network control
- good ergonomics for modern frontend teams already using TypeScript or JavaScript
Choose Selenium when you need
- broad browser automation inside an established Selenium ecosystem
- legacy framework compatibility
- existing investment in proxies, grids, and service virtualization
- language and runner flexibility across older test suites
- browser interaction without needing first-class request mocking in the framework
Choose a different model when you need
- to reduce code ownership for mocked backend flows
- non-developers to author or maintain test scenarios
- stable, human-readable test steps instead of framework code
- a managed platform with less interception logic to maintain
That last point is where a platform like Endtest can be relevant. For teams that want stable mocked scenarios without carrying heavy interception code, a low-code or no-code platform can shift the burden from framework plumbing to editable test steps. Endtest’s agentic AI approach is also positioned around creation and maintenance inside the platform, which can matter when the main problem is not capability but long-term ownership.
How to keep mocked UI tests from becoming misleading
Mocked UI tests fail quietly when they stop representing real usage. A few guardrails help.
Keep one or more live-path tests
Even if most UI scenarios use mocked responses, maintain some end-to-end tests against a real backend or staging environment. These tests verify that routing, authentication, serialization, and deployment configuration still work together.
Version the fixtures with the API
If your team owns the API, treat fixtures as code that evolves alongside schemas. When responses change, update the test doubles in the same pull request or in a tightly coupled change set.
Assert observable behavior, not internal implementation
Check what the user can see or do, such as visible text, enabled buttons, and rendered data. Avoid turning the test into a duplicate of the server’s business rules.
Prefer small scenario builders over giant JSON dumps
Large static payloads are hard to maintain. Builders let you create focused responses for specific states, such as empty, partial, error, delayed, unauthorized, or stale data.
Isolate side effects
If a test mocks a request that is triggered multiple times, be explicit about whether each call gets the same response or different responses. Retry logic can otherwise hide unexpected behavior.
CI implications, especially for flaky networks and external services
In Continuous integration, network interception can reduce one class of flakiness while introducing another. By mocking unstable services, you remove dependency on availability and latency. But if the mock setup is inconsistent across local runs, CI runs, and parallel workers, you just moved the flakiness.
A few practical points:
- Keep mock registration deterministic.
- Make fixture data explicit in test names or helper names.
- Avoid hidden global setup that affects unrelated tests.
- If using Selenium plus external stubs, document the order of startup for browser, proxy, API stub, and app server.
- Record whether a failure came from the UI, the mock, or the test harness.
The value of a good interception strategy is that it narrows the search space when something fails. If the search space is still large, the mocking layer may be too indirect.
Where Cypress fits, briefly
Cypress also supports network control and request stubbing, so it belongs in the same conversation. The key distinction is that Cypress’s architecture is browser-centric with a built-in command queue, which many frontend teams find approachable. If your main decision is between Playwright and Selenium, Cypress is worth considering when your team prefers a browser-first workflow and is comfortable with its app model. The same evaluation criteria still apply, though, fixture clarity, scope of interception, and how much test behavior lives inside the framework.
A practical selection guide
If your tests are mostly about UI state driven by controlled backend responses, ask these questions:
- Do we need to intercept requests at the browser layer? If yes, Playwright has the most direct support.
- Do we already have a backend stub or environment strategy that works? If yes, Selenium can remain viable.
- How many scenarios require unique response shapes? The more scenarios you need, the more painful indirect mocking becomes.
- Who will maintain the fixtures six months from now? If that answer is unclear, favor a simpler model.
- Do we need non-developers to inspect or adjust the tests? If yes, a managed platform may reduce ownership concentration.
Conclusion
For network interception, API mocking, and contract-style UI tests, Playwright is usually the most ergonomic code-based choice because the browser request layer is first-class and the test can describe its own backend assumptions directly. Selenium can still support the same goals, but usually through proxies, backend test environments, or browser-specific workarounds. That makes Selenium a better fit when you already have the infrastructure and team habits to support those patterns, not when you are starting from a blank slate.
The deeper decision is not just framework preference. It is whether your team wants to own interception logic, fixture builders, route scoping, and CI plumbing, or whether a managed platform should absorb more of that complexity. For teams in the latter category, Endtest can be a simpler route to stable backend-dependent UI scenarios, especially when the priority is maintainability over custom framework code. For teams evaluating that path, the comparison with Selenium is a useful place to start, particularly if the current pain is fixture upkeep rather than raw browser capability.
In short, the strongest setup is the one that gives you deterministic UI behavior, clear test intent, and a maintenance cost your team can actually carry.