API Technique · Senior

WebSocket Testing

WebSockets enable real-time bidirectional communication — live dashboards, notifications, chat, collaborative editing. They behave nothing like REST. Standard API testing tools don’t work. This entry teaches you to test WebSocket connections, message formats, and real-time behaviour.

Senior Senior SDET ~20 min read · ~50 min with exercises

1 The Hook

A Wellington logistics company builds a live shipment tracking dashboard. The API tests all pass. The data is correct. They go live.

The project manager notices the dashboard updates only on page refresh — not in real time as the spec promised. Investigation reveals: the WebSocket connection drops silently after 30 seconds due to a server idle timeout misconfiguration. Every 30 seconds, the connection dies and the client never reconnects.

The REST tests had tested the data correctly. They never tested the WebSocket connection that should have been pushing the updates. No WebSocket tests existed. A feature that was core to the product’s value proposition had shipped with a silent failure mode that no test had ever touched.

The fix took two hours. The missed test took months to recover from commercially.

2 The Rule

WebSocket tests must verify three things independently: connection establishment, message format (both directions), and connection lifecycle — idle timeout, reconnection, and disconnect behaviour. Testing only the REST API behind a real-time feature is not WebSocket testing.

3 The Analogy

Analogy

Testing a WebSocket by testing the REST API is like testing a phone call by reading the phone book.

The data is right — you have the number — but you have not tested whether the call connects, whether both parties can hear each other, or whether the line drops after 30 minutes of silence. The phone book is correct. The phone call is a completely different thing.

WebSocket is a persistent, bidirectional protocol running over a separate connection from HTTP. REST tests tell you nothing about it. You need a different test for a different thing.

4 Watch Me Do It

Playwright has built-in WebSocket event listeners. Use them to capture and assert on frames without any additional tooling.

import { test, expect } from '@playwright/test'; test('shipment tracking WebSocket sends live updates', async ({ page }) => { // Collect WebSocket frames as they arrive const wsMessages: string[] = []; page.on('websocket', ws => { console.log(`WebSocket opened: ${ws.url()}`); ws.on('framesent', frame => wsMessages.push(`SENT: ${frame.payload}`)); ws.on('framereceived', frame => wsMessages.push(`RECV: ${frame.payload}`)); ws.on('close', () => console.log('WebSocket closed')); }); await page.goto('/tracking/NZ-SHIP-001'); // Trigger a status update via the REST API await page.request.post('/api/shipments/NZ-SHIP-001/status', { data: { status: 'IN_TRANSIT', location: 'Auckland Port' } }); // Assert the WebSocket pushed the update within 5 seconds await expect.poll( () => wsMessages.some(m => m.includes('IN_TRANSIT')), { timeout: 5000, message: 'Expected WebSocket to receive IN_TRANSIT update' } ).toBeTruthy(); // Verify the UI updated without a page refresh await expect(page.getByTestId('shipment-status')).toContainText('In Transit'); });

Test reconnection after a network interruption:

test('WebSocket reconnects after connection drop', async ({ page }) => { let connectionCount = 0; page.on('websocket', () => connectionCount++); await page.goto('/tracking/NZ-SHIP-001'); expect(connectionCount).toBe(1); // Simulate network interruption await page.context().setOffline(true); await page.waitForTimeout(2000); await page.context().setOffline(false); // App should reconnect automatically await expect.poll( () => connectionCount >= 2, { timeout: 10000, message: 'Expected WebSocket to reconnect' } ).toBeTruthy(); });

Manual testing tools

  • wscatnpm install -g wscat then wscat -c ws://localhost:3000/tracking. Connect to any WebSocket from the terminal and send/receive frames manually.
  • Postman — Collection › New › WebSocket Request. Good for exploring message schemas and testing auth headers.
  • Browser DevTools — Network tab › WS filter. Shows all frames, connection timing, and close codes without any extra tooling.
Pro tip: Check the WebSocket close code on disconnect. Code 1000 is a normal close. Code 1006 (abnormal closure) with no close frame is the server dropping the connection silently — the most common production failure mode.

5 When to Use It

Any feature using real-time communication needs WebSocket-specific tests. Common NZ examples: live auction bidding, banking transaction notifications, freight tracking dashboards, collaborative document editing, real-time support chat.

Your WebSocket test plan must cover all of these independently:

  • Connection establishment — does the client connect on page load?
  • Message schema validation — do received messages match the expected JSON shape?
  • Message delivery latency — does the update arrive within an acceptable window?
  • Heartbeat / ping-pong — does the client respond to server pings to keep the connection alive?
  • Idle timeout — what happens after 30–60 seconds with no messages?
  • Reconnection — does the client reconnect after a drop, and how quickly?
  • Error messages — does the server send meaningful error frames for bad requests?
  • Multi-client isolation — does one client’s disconnect affect others?

6 Common Mistakes

🚫 Assuming REST API coverage covers the real-time feature

What I used to think: If the REST API returns the right data, the real-time feature works.
Actually: WebSocket is a separate protocol over a separate persistent connection. REST tests tell you nothing about WebSocket connection establishment, message delivery, idle timeout behaviour, or reconnection logic. The logistics company in the hook had correct REST tests and a broken real-time feature simultaneously.

🚫 Thinking WebSocket testing requires specialist tools

What I used to think: WebSocket testing is hard and needs expensive tools.
Actually: Playwright has built-in WebSocket event listeners that work out of the box. For manual exploration, wscat (npm install -g wscat) lets you connect to any WebSocket from the terminal. For contract testing, you can assert on JSON message schemas directly in your Playwright tests.

🚫 Only testing that messages arrive, not the connection itself

What I used to think: I only need to check that the UI updates when messages come in.
Actually: WebSocket connection issues — idle timeouts, dropped connections without reconnect, memory leaks from unclosed connections — are the most common production failures in real-time features. The connection lifecycle is a first-class test concern. Test it explicitly and separately from message content.

7 Now You Try

Write your answer, run it for AI feedback, then check the model answer.

📡 Exercise — Real-Time Auction WebSocket Tests

A NZ real-time auction platform uses WebSockets to push bid updates to all participants. Write 4 test cases: (1) a bid update message is received within 500ms of being placed, (2) the message schema is correct (bidAmount, bidderId, timestamp), (3) the connection reconnects within 5 seconds after a network drop, and (4) disconnecting one participant does not affect other participants’ connections.

Show model answer
Test 1 — Bid update within 500ms:
const messages: string[] = [];
page.on('websocket', ws => ws.on('framereceived', f => messages.push(f.payload as string)));
await page.goto('/auction/NZ-LOT-042');
const start = Date.now();
await page.request.post('/api/bids', { data: { lotId: 'NZ-LOT-042', amount: 1500 } });
await expect.poll(() => messages.some(m => m.includes('NZ-LOT-042')), { timeout: 500 }).toBeTruthy();
expect(Date.now() - start).toBeLessThan(500);

Test 2 — Message schema validation:
const received: any[] = [];
page.on('websocket', ws => ws.on('framereceived', f => {
  try { received.push(JSON.parse(f.payload as string)); } catch {}
}));
await page.goto('/auction/NZ-LOT-042');
await page.request.post('/api/bids', { data: { lotId: 'NZ-LOT-042', amount: 1500 } });
await expect.poll(() => received.length > 0, { timeout: 2000 }).toBeTruthy();
const msg = received[received.length - 1];
expect(msg).toHaveProperty('bidAmount');
expect(msg).toHaveProperty('bidderId');
expect(msg).toHaveProperty('timestamp');
expect(typeof msg.bidAmount).toBe('number');
expect(typeof msg.bidderId).toBe('string');
expect(Date.parse(msg.timestamp)).not.toBeNaN();

Test 3 — Reconnection within 5 seconds:
let connections = 0;
page.on('websocket', () => connections++);
await page.goto('/auction/NZ-LOT-042');
expect(connections).toBe(1);
await page.context().setOffline(true);
await page.waitForTimeout(1000);
await page.context().setOffline(false);
await expect.poll(() => connections >= 2, { timeout: 5000, message: 'Should reconnect within 5s' }).toBeTruthy();

Test 4 — Disconnect isolation:
// Use two browser contexts to simulate two participants
const context2 = await browser.newContext();
const page2 = await context2.newPage();
let page2Messages: string[] = [];
page2.on('websocket', ws => ws.on('framereceived', f => page2Messages.push(f.payload as string)));
await page.goto('/auction/NZ-LOT-042');
await page2.goto('/auction/NZ-LOT-042');
// Close page1 (participant 1 disconnects)
await page.close();
// Participant 2 should still receive bid updates
await page2.request.post('/api/bids', { data: { lotId: 'NZ-LOT-042', amount: 1600 } });
await expect.poll(() => page2Messages.some(m => m.includes('1600')), { timeout: 3000 }).toBeTruthy();
await context2.close();

8 Self-Check

Click each question to reveal the answer.

Q1: What Playwright API do you use to listen to WebSocket frames during a test?

page.on('websocket', ws => { ... }) gives you the WebSocket object. From there, ws.on('framereceived', frame => ...) captures server-to-client messages and ws.on('framesent', frame => ...) captures client-to-server messages. Use expect.poll() to wait for specific frames to arrive asynchronously.

Q2: Name three WebSocket-specific failure modes that REST API tests would miss.

(1) Idle timeout — the server closes the connection after a period of inactivity and the client does not reconnect. (2) Silent connection drop — the connection drops without sending a close frame (close code 1006), and the client UI freezes silently. (3) Memory leak from unclosed connections — each page navigation opens a new WebSocket but the old one is never explicitly closed, leading to resource exhaustion under load.

Q3: What is a WebSocket ping/pong frame and why should you test for it?

A ping frame is sent by one side (usually the server) to check whether the connection is still alive. The other side must respond with a pong frame. This heartbeat mechanism keeps the connection open through NAT gateways and proxies that would otherwise close idle TCP connections. You should test that your client responds to pings correctly — failure to do so means the connection will be silently dropped by network infrastructure after a few minutes of low traffic.

9 ISTQB Mapping

CTAL-TA v3.1.2 Section 3.2.4 — Testing web services, including WebSocket as a stateful, persistent communication protocol distinct from request-response HTTP.

CTAL-TA v3.1.2 Section 3.2.7 — Non-functional testing: performance and reliability of real-time connections, including latency thresholds and reconnection behaviour.