skip to content

In Playwright, when should a suite fully mock a WebSocket instead of routing it to the real server?

level: principalimportance: should knowfreq 28%

answer

  1. Three postures, not a binary
  2. Determinism versus fidelity
  3. Unreachable states argue for mocking
  4. A mock is a snapshot of a contract
  5. Harvest fixtures, never invent them

basics

~20 s

Mock fully when determinism, offline runs and speed matter more than fidelity, which covers the bulk of UI cases. Connect to the real server when the handshake is complex or integration itself is the point.

solid answer

~40 s

Treat it as three postures, not two. A **full mock** — `page.routeWebSocket()` with no `connectToServer()` — buys total determinism, offline CI and speed, and is right for the large body of tests about how the dashboard renders what arrives. **Pass-through with injection** — `connectToServer()` plus `onMessage()` on one side — keeps real traffic flowing while the test adds the rare frame, and suits an undocumented handshake or a smoke test in a real environment. **Observation only** — `page.on('websocket')` — verifies what the app exchanged without touching it. The cost of the first is drift: hand-written frames are a snapshot of a contract nobody re-checks. So pair a mocked UI suite with something that watches the real payload shape, and harvest fixtures from recorded frames rather than inventing them.

code

typescript · 18 lines
typescript
import { test, expect } from '@playwright/test';
import { readFileSync } from 'node:fs';

test('renders the recorded forecast sequence', async ({ page }) => {
  const frames: string[] = JSON.parse(
    readFileSync('fixtures/forecast-frames.json', 'utf8'),
  );

  await page.routeWebSocket(/\/subscribe/, ws => {
    ws.onMessage(() => {
      for (const frame of frames) ws.send(frame);
    });
  });

  await page.goto('/dashboard');

  await expect(page.getByTestId('temperature')).toHaveText('21°C');
});

go deeper

for a junior

Know that Playwright can either fully mock a socket or let it reach the real server, and that mocked frames make a test repeatable because nothing depends on a live feed.

for a middle

Explain the middle option: connectToServer with onMessage on one side forwards real traffic while the test injects a frame, which suits handshakes you would rather not reimplement.

for a senior

Argue the split concretely for a suite: broad mocked UI coverage, a thin connected smoke set, fixtures harvested from real frames, and route setup shared through a fixture.

for a principal

Own the drift risk and where it is caught. Decide who refreshes fixtures, which layer detects a changed contract, and how connected tests stay excludable when the feed is down.

## Three postures, not two The choice is usually framed as mock-or-don't, which hides the middle option that most mature suites end up using. | Posture | How | Buys you | Costs you | |---|---|---|---| | Full mock | `page.routeWebSocket()`, no `connectToServer()` | Determinism, offline runs, speed, unreachable states | Frames drift from the real contract | | Pass-through with injection | `connectToServer()` plus `onMessage()` on one side | Real traffic and real handshake, one synthetic frame | Depends on a live feed; inherits its timing | | Observation only | `page.on('websocket')` | Evidence of what was really exchanged | No control over anything | ## When a full mock earns its keep - **The test is about the UI, not the feed.** A weather dashboard has dozens of cases — units, rounding, stale-data styling, an alert banner, an empty city — that are all about rendering a payload. The feed is scenery. - **The state is unreachable on demand.** A severe-weather alert that fires twice a year cannot be triggered by waiting; a mock is the only honest way to test the banner. - **CI has no network.** Runs must not depend on a third-party endpoint being up, fast, or willing. - **Parallelism.** A hundred workers hammering a live feed is a load test nobody ordered, and shared state between workers makes failures irreproducible. ## When to keep the real server in the loop - **The handshake is complex or undocumented.** Reverse-engineering subscribe, auth and heartbeat frames to mock them is an afternoon that buys nothing; `connectToServer()` lets them through untouched. - **The question is integration.** A small smoke suite that asserts the dashboard really connects, subscribes and renders something proves what no mock can. - **You need one branch inside a real conversation.** Forwarding every real frame and adding one synthetic alert tests the banner in an otherwise production-faithful session. ## The drift problem, and how to blunt it A hand-written frame is a snapshot of a contract taken on the day someone wrote it. When the feed renames a field, every mocked test still passes and the product breaks. Nothing in Playwright detects this; a strategy has to. 1. **Harvest, don't invent.** Capture real payloads once with `page.on('websocket')` and `framereceived`, and store them as fixtures rather than typing plausible JSON. 2. **Own the shape somewhere.** A schema check, a consumer-driven contract test, or a small pass-through smoke run gives the drift somewhere to be caught. 3. **Re-harvest on a schedule.** Refreshing fixtures is cheap when it is a script and expensive when it is archaeology. 4. **Fail loudly on shape.** Validate a fixture against the app's parser rather than letting a stale field render as `undefined`. ## Cost lands on people, not machines A mock is code someone maintains. The realistic question at portfolio level is not "is this test faster" but "who notices when this fixture becomes a lie, and how long does it take them". A suite with two hundred mocked socket tests and no pass-through check is fast, green, and silently decoupled from the product. - Keep the mocked layer broad and the connected layer thin — a handful of cases, not a mirror of the UI suite. - Put route setup in a shared fixture so the payload shape lives in one place and can be updated once. - Tag or project-split the connected tests so an offline or unstable-feed run can exclude them without disabling the whole suite. ## A default worth defending Mock by default; connect deliberately, in named cases, for reasons someone wrote down. The inverse — connecting by default because it feels more honest — produces a suite whose failures are mostly about somebody else's uptime, and a team that learns to re-run red tests instead of reading them. ## Signals that the balance is wrong - Incidents a mocked suite structurally could not have caught — a renamed field, a new auth requirement on the channel — mean the connected layer is too thin. - Reruns as a reflex, where a red socket test is retried rather than read, mean the connected layer is too broad or its assertions are too specific about values a live feed controls. - Fixtures whose provenance nobody can explain mean the harvesting step was skipped and the frames were written from memory, which is the drift problem already in progress.

  • How do you stop a mocked socket suite from drifting away from the real feed?
    Harvest fixtures from recorded `framereceived` payloads rather than writing them by hand, refresh them with a script on a schedule, and keep a thin pass-through suite that connects with `connectToServer()` so a changed field or renamed channel fails somewhere. Validating fixtures against the app's own parser catches the rest.
  • Which socket tests would you keep connected to the real server in CI?
    A small, named set: connect, subscribe, render one real update. Keep them in their own project or tag so an offline run can exclude them, and keep assertions structural — that a value arrived and rendered — rather than about specific numbers a live feed controls.
  • What signals that a team has mocked too much?
    Production incidents that no test could have caught: a renamed field, a changed envelope, a channel that now requires auth. Green suites paired with recurring integration surprises mean the mocked layer has become a description of the past rather than of the system.

A full mock is a flight simulator: you can practise the engine fire that never happens on a real route, but the cockpit only stays useful if someone keeps updating it to match the actual aircraft.

saying these in an interview costs you the question

  • Treats the choice as mock or nothing, ignoring pass-through
  • Writes mock frames from imagination rather than recorded traffic
  • Runs the whole suite against a live third-party feed
  • Keeps no check that the real payload shape still matches
  • Assumes a green mocked suite proves integration works