Back to Knowledge Hub
Getting Started

Browser Automation Network Mocking for Owned Tests

Use controlled network doubles to make authorized browser tests deterministic while keeping production and third-party boundaries explicit.

Documentation

Want the structured docs for Getting Started?

This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.

An owned browser test routes a synthetic request through a controlled response and records a bounded result

Network mocking is useful when a browser test owns both the page under test and the application dependency it exercises. A controlled response can make a loading state, empty result, validation error, or temporary outage reproducible without waiting for a remote service. The boundary is part of the test contract: the mock is a test double for an authorized application dependency, not a way to alter a third-party site, ignore a policy, or claim that production behaved the same way.

Start with the user-facing contract. Name the route, request method and shape, expected visible state, and evidence that the test may retain. Then decide which dependency the test owns and which real integration still needs a separate check. See the Playwright setup guide and download and upload testing guide for lifecycle and artifact context. Playwright's API mocking guide and Selenium's test practices describe framework mechanics; neither makes a synthetic response proof of a remote service.

Define the ownership boundary

Intercept only a request that belongs to the test scenario. Use a synthetic account, a test origin, and a route pattern narrow enough to avoid catching analytics, authentication, telemetry, or unrelated assets. A handler should inspect the method and relevant request fields, return a documented response, and record only a short outcome. Do not retain authorization headers, full bodies, cookies, page text, or a copied production response body merely because the handler can see them.

Keep the production path separate. Put route handlers in test code or a test-only server, select them through an explicit fixture option, and fail the test if that option is accidentally enabled in a production build. Name the scenario in the result so a reviewer can distinguish a synthetic response from a live integration. If the application uses a service worker or cache, document whether the test clears or uses it. A handler that never receives a request is evidence about the browser path, not proof that the service was called.

Define a deterministic fixture

Before writing the handler, write down four small facts:

  • Input: a synthetic record, route, method, and response schema owned by the test.
  • Change: one controlled condition such as status 503, an empty list, or a bounded delay.
  • Observation: a user-visible state and a short matched-route count.
  • Cleanup: route removal, context closure, and artifact retention status.

One changed condition keeps failures explainable. If the same fixture also changes authentication, retries, cache state, and database data, a passing assertion cannot identify which contract was exercised. Keep the response schema versioned with the test and use values that cannot be mistaken for a customer record. A fixture can represent an upstream incident, but its name should say that it is synthetic.

Choose the smallest network API

The following matrix maps one testing question to one interception surface and its evidence limit. Use one row per contract rather than combining all traffic into a catch-all handler.

QuestionPlaywrightSelenium/WebDriverEvidence boundary
Return a known JSON successpage.route('**/api/items', route => route.fulfill({ json }))A test proxy or owned stub endpoint configured before the driver startsRendered state for the synthetic response
Exercise a server errorroute.fulfill({ status: 503, body: ... })Stub endpoint returns the documented statusError UI and recovery action, not service health
Simulate latencyroute.fulfill({ delay: 250, ... }) or a controlled stubProxy or stub delays only the named routeLoading transition and timeout handling
Observe without changing trafficroute.continue() plus a redacted counterProxy log with request metadata onlyBrowser request path, not authorization success
Ensure teardownpage.unroute() in finally and close the contextStop the owned proxy and quit the driverNo handler or worker state leaks to the next test

Do not use a broad **/* interception unless the test explicitly verifies a network policy. Broad handlers can hide missing assets and make a green result unrelated to the real journey. For one route, assert the method, return a small fixture, and let other requests continue or fail according to the scenario's contract. If a request is unexpectedly unmatched, make that a visible failure instead of silently inventing a response.

Keep a reproducible failure fixture

A failure fixture should change one condition and name the expected user-visible result. This Playwright example returns a synthetic outage, preserves the first assertion failure, and always removes the handler with the context:

const context = await browser.newContext();
const page = await context.newPage();
let matched = 0;
await page.route('**/api/items', async route => {
  matched += 1;
  await route.fulfill({
    status: 503,
    contentType: 'application/json',
    body: JSON.stringify({ code: 'owned-test-outage' }),
  });
});
try {
  await page.goto('http://test.local/items');
  await expect(page.getByRole('alert')).toHaveText('Items are temporarily unavailable');
  expect(matched).toBe(1);
} finally {
  await page.unroute('**/api/items');
  await context.close();
}

The fixture proves that the page renders its documented error and recovery path for this response. It does not prove that a real upstream emits status 503, that retries are safe, that authorization succeeded, or that a remote record was unchanged. Keep the synthetic code and response schema beside the test. Never copy a production response body containing secrets into a fixture.

If setup, assertion, and cleanup can all fail, retain the first workflow error and report cleanup as a separate field. A later cleanup error must not replace the reason the page failed. The same rule applies to retries: a retry is a new context and a new attempt. A later pass does not prove that the first mocked or real request had no remote effect.

Decide when realism wins

Mocking is a scoped tradeoff. Use this decision table before adding a handler:

Test purposeMock the dependency?Why
Verify a loading, empty, validation, or outage state owned by the pageYesDeterministic input makes the UI contract repeatable
Check serialization and a supported client/server contractUsually no; use an owned integration environmentA mock cannot catch schema or transport drift
Validate a payment, identity, or other irreversible operationNo for the final assertionOnly the authorized service can establish the remote result
Reproduce a known upstream incident in a local testYes, with a named fixtureThe synthetic condition is explicit and reviewable
Probe or alter a third-party serviceNoOutside the test's ownership and authorization boundary

Pair mocked tests with a small number of real integration checks. A mocked test can run on every change; an integration check can verify that the route, headers, schema, authorization, and service policy still agree in an approved environment. Keep their names and evidence separate so a pass in one lane cannot mask a failure in the other. A browser-visible response is not evidence of a billing event, account change, data deletion, or remote availability unless the service owner supplies that evidence.

Isolate contexts, data, and artifacts

Create a fresh BrowserContext when cookies, storage, permissions, cache, or service workers can affect the route. Give each worker a private artifact directory and synthetic identifiers. A context prevents browser-managed state from leaking between tests; it does not isolate a shared database, queue, or proxy process. If two tests mutate one record, use independent records or serialize that mutation through an application-supported operation.

Treat fixture files and route labels as owned inputs. A read-only baseline may be copied into a worker directory, but a partially written result must not replace the baseline another worker is reading. Keep screenshots, traces, and request counters separate, and apply the ordinary retention policy. A short receipt should contain only the scenario, matched route, response class, visible result, and cleanup status.

Teardown belongs in finally. Remove the route, close pages and contexts, stop an owned proxy, and report cleanup failures beside the original assertion. If a service worker or cache keeps serving a response, record that observed path and change the fixture setup deliberately. Do not broaden interception just to force a match.

Keep production and test traffic distinct

Use a test hostname or an application-supported test mode, synthetic identities, and a separate data store where possible. Make the switch to mocking explicit in the runner configuration and visible in CI logs. The production build should not import test handlers, and a production smoke check should fail closed if a test-only option is present. This is a deployment boundary, not merely a naming convention.

When a real integration check is required, run it with the service owner's approved account and retention policy. Do not reuse a mock receipt as proof of that check. Compare the same user-visible contract, then label which facts came from the browser and which came from the service. If the live route is unavailable, record the external condition instead of replacing it with a synthetic pass.

BotBrowser capability and limitation

BotBrowser can provide an isolated BrowserContext with separate browser-side state for an authorized test. That is useful when a network fixture must start from known cookies, storage, permissions, or a clean service-worker state. BotBrowser does not decide which routes a test may intercept, does not make third-party traffic yours to modify, and does not verify a remote service's database, billing record, authorization decision, or side effects. The test owner still supplies synthetic data, controls the handler, closes the context, and asks the application owner for service evidence outside the browser boundary.

Keep the capability and limitation together in the test report. Record the context purpose, browser and framework versions, matched route, response class, visible outcome, and cleanup receipt. State that the result came from a synthetic dependency. Never present a mocked pass as proof of production availability, authorization, or a successful business transaction. A clean context also does not prove that a server session was revoked or that a remote queue was drained.

Review the boundary before merging

Check that the handler matches one owned route, the fixture changes one condition, unmatched traffic follows an intentional policy, and cleanup runs after setup and assertion failures. Run one mocked success, one forced failure, and one approved real integration case when the application contract requires it. Inspect the fresh result for the route label and cleanup receipt, not a dumped request body.

Ask a reviewer to confirm four non-claims explicitly: the fixture does not modify a third-party service; the response does not prove upstream health; browser isolation does not delete server data; and a retry does not erase uncertainty about the first attempt. This keeps network evidence useful, private, and proportional to the browser behavior under test.

Choose route ownership before choosing syntax. A URL that looks like an application endpoint may still be served by a vendor, an identity provider, or a shared platform team. Write the owner and approved environment in the test description. If ownership is unclear, leave the request live in an approved integration lane and assert only the browser behavior that the team is allowed to observe.

Keep request matching explicit. Match the HTTP method, the path, and only the query fields that define the scenario. A route that matches every method can accidentally turn a harmless read into a synthetic write. A route that ignores a version field can hide a client upgrade. Small match predicates make review easier and make an unexpected request fail close to its cause.

Treat response classes as part of the contract. A success response should include only the fields needed by the page, while an error response should use the documented error shape. Do not make a mock more complete than the service contract. Extra fields can hide an accidental dependency, and missing fields can turn a UI test into an undocumented schema test.

Make time observable without making it fragile. A bounded delay can exercise a spinner or timeout, but assertions should wait for a state transition rather than a fixed sleep. Keep the delay value in the fixture name and receipt. When a timeout is the expected result, distinguish it from a route that was never matched or a page that failed to start.

Use one receipt format across mocked and live lanes. The receipt can contain the scenario name, route pattern, method, response class, visible state, match count, and cleanup result. It should not contain credentials, cookies, full request bodies, or private page text. Consistent receipts let a reviewer compare evidence without mistaking a browser observation for a service-side fact.

Retries deserve their own assertion. A retry may be useful for a transient UI state, but it must create a fresh attempt and retain the first attempt outcome. If the first attempt might have reached a real service, mark that uncertainty instead of reporting a clean pass. A mock can test the retry control flow; only an approved integration can establish the remote side effect.

Finally, keep the fixture close to the assertion that explains it. Name the synthetic condition, link to the user-visible contract, and document the approved real check that covers transport or service behavior. This makes the test readable months later and prevents a convenient mock from becoming an accidental production guarantee.

The same boundary applies to authentication and authorization flows. A test may use a synthetic session owned by the application team, but it should not copy a customer session into a route fixture. Represent approved test roles explicitly and keep their permissions in the test environment. A mocked 401 or 403 can check the page response; it cannot establish that a real policy engine made the same decision.

Cache behavior deserves a deliberate choice. Clearing a cache can make a fixture deterministic, while preserving it can verify that the page handles a cached response. State the choice in the scenario and record whether the route was matched. If a cache serves the response without a route match, keep that observation instead of widening the route.

For parallel execution, make ownership visible in the fixture name and artifact path. Two workers should not share a mutable receipt, temporary response file, or synthetic record. A private context protects browser-managed state, but application data still needs an independent key or an application-supported serialization rule. This prevents browser teardown from being mistaken for database cleanup.

The same boundary applies to authentication and authorization flows. A test can use a synthetic session owned by the application team, but it should not copy a customer session into a route fixture. If an access decision is the behavior under test, represent approved test roles explicitly and keep their permissions in the test environment. A mocked 401 or 403 can check the page response; it cannot establish that a real policy engine made the same decision.

Cache behavior deserves a deliberate choice. Clearing a cache can make a fixture deterministic, while preserving it can verify that the page handles a cached response. State the choice in the scenario and record whether the route was matched. If a cache or worker serves the response without a route match, keep that observation instead of widening the route until it matches.

For parallel execution, make ownership visible in the fixture name and artifact path. Two workers should not share a mutable receipt, temporary response file, or synthetic record. A private context protects browser-managed state, but application data still needs an independent key or an application-supported serialization rule. This distinction prevents a clean browser teardown from being mistaken for database cleanup.

Reviewers should be able to answer three questions from the result alone: which dependency was synthetic, what visible contract was checked, and what remained outside the browser boundary. A compact receipt and a named scenario answer those questions without exposing private request content. When a result needs service-side confirmation, link that approved check rather than adding more interception.

Sources

#Browser Automation#Network Mocking#Playwright#Test Doubles#Deterministic Tests

Take BotBrowser from research to production

The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.