intermediate20 min

Network Observation and Mocking

Watching real network traffic a page generates, and deliberately replacing part of it — the difference between observing and mocking, and when each is the right tool.

What you'll learn

  • Wait for and assert against a specific network response triggered by a UI action
  • Mock a network response to test a UI state that's hard to reach naturally (an error, an empty result)
  • Explain the tradeoff between testing against a real backend and mocking network responses

Prerequisites

Explanation

Observing network traffic means watching real requests/responses the application genuinely makes, without altering them: const responsePromise = page.waitForResponse(resp => resp.url().includes("/api/courses") && resp.status() === 200); await page.getByRole("button", { name: "Load courses" }).click(); const response = await responsePromise; — same race-avoidance pattern as popups and downloads (start waiting before the triggering click), letting a test assert against the real response's status, timing, or body without ever pretending the network call didn't happen.

Mocking deliberately replaces a network response with a fabricated one, via page.route(urlPattern, handler): await page.route("**/api/courses", route => route.fulfill({ status: 500, body: JSON.stringify({ error: "Internal error" }) })). This is the practical, honest way to test UI states that are difficult or impossible to reliably reach against a real backend — a server error, an empty result set, a specific edge-case response shape — without needing to actually break a real server or seed exact, fragile backend state for every single scenario. route.continue() lets a request through unmodified (useful for observing without altering); route.fulfill(...) replaces the response entirely; route.abort() simulates the request failing outright (a network error, not an HTTP error status).

The honest tradeoff worth stating explicitly: mocking makes a test fast, deterministic, and able to reach states a real backend can't reliably produce on demand — but a test built entirely on mocks only proves the frontend behaves correctly given that exact mocked response shape; it says nothing about whether the real backend actually returns that shape, or whether the real integration between frontend and backend genuinely works end to end. A mature test suite typically uses both: some tests genuinely exercise the real backend (proving real integration), and some use mocks specifically for hard-to-reach states (proving the frontend handles them correctly) — treating mocking as a replacement for all real-backend testing, rather than a complement to a smaller number of real ones, is a common design mistake that can let a real integration break while every mocked test keeps passing.

Example

Modeling route interception's three outcomes (continue/fulfill/abort) and the observe-vs-mock distinction as data.

function applyRouteHandler(request, mode, mockResponse) {
  if (mode === "continue") {
    return { type: "real-network-call", request };
  }
  if (mode === "fulfill") {
    return { type: "mocked-response", body: mockResponse };
  }
  if (mode === "abort") {
    return { type: "network-error", request };
  }
  throw new Error("unknown route mode: " + mode);
}

console.log(applyRouteHandler("/api/courses", "continue", null));
console.log(applyRouteHandler("/api/courses", "fulfill", { error: "Internal error" }));
console.log(applyRouteHandler("/api/courses", "abort", null));

Try it yourself

Call applyRouteHandler with mode 'bogus' and observe it correctly throws, rather than silently doing nothing.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Write buildMockResponse(status, body) that returns an object { status, body: JSON.stringify(body) } -- modeling exactly what route.fulfill({...}) needs. Then write shouldMockOrObserve(scenario) returning 'mock' for 'server-error' or 'empty-results' (hard to reliably reach against a real backend), or 'observe' for 'happy-path' (should exercise the real integration).

Checks: builds a correct mock response object · recommends mocking for a hard-to-reach state · recommends observing (real backend) for the happy path

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write matchesUrlPattern(url, pattern) implementing a SIMPLIFIED version of Playwright's glob-style route matching: pattern may contain '**' meaning 'match anything (including slashes)'. Split the pattern on '**' and check that url starts with the part before it and ends with the part after it (if either part is non-empty).

Checks: correctly rejects a non-matching URL · correctly matches a URL ending with the pattern's suffix · correctly rejects a URL with extra trailing content past the expected suffix

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Mocking every single network call in a test suite -- this makes tests fast and deterministic, but a suite with no real-backend tests at all proves nothing about whether the actual integration works, only that the frontend handles fabricated responses correctly.
  • Using route.abort() when route.fulfill() with an error status was intended, or vice versa -- abort() simulates a network-level failure (no response at all), while fulfill() with a 500 status simulates a real HTTP error response; these are genuinely different failure modes worth testing separately.
  • Waiting for a network response AFTER triggering the action that causes it, instead of using the same before-the-trigger pattern from the previous lesson -- the exact same race condition applies to network responses as to popups and downloads.

Knowledge check

Knowledge check

1. What is the key difference between 'observing' and 'mocking' network traffic in a Playwright test?
2. Why is testing a UI's error-handling entirely through mocked network responses, with no real-backend tests at all, a genuine risk?
3. What does route.abort() simulate, as distinct from route.fulfill({ status: 500, ... })?

Takeaway

Observing watches real network traffic; mocking deliberately fabricates it via page.route — mocking is the honest way to reach hard-to-produce states like server errors, but a suite that mocks everything proves nothing about real integration, so a mature suite deliberately uses both.

Summary

waitForResponse observes real network traffic, using the same before-the-trigger race-avoidance pattern as popups/downloads. page.route intercepts requests: continue() passes through, fulfill() substitutes a fabricated response, abort() simulates a network-level failure. Mocking and real-backend testing are complementary, not interchangeable.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.