Testing WebSocket Connections Automatically
AI generated
PASS
expect()
WebSocket Testing · Real-Time Communication
Testing WebSocket Connections Automatically
How connection setup, message exchange, and reconnect behavior get tested reliably and automatically

A WebSocket connection differs fundamentally from a classic HTTP request, because it isn't a single request with exactly one response but a persistent, bidirectional connection over which multiple messages flow in both directions at unpredictable times. This fundamental property makes classic request-response test patterns unsuited and calls for dedicated test approaches for connection setup, ongoing message exchange, and behavior on an unexpected connection drop.

15 min read WebSocket Testing Real-Time Communication

1. Why WebSocket testing differs fundamentally from HTTP request testing

A classic HTTP test follows a simple, linear pattern: a request gets sent, the test waits for exactly one response, and that response gets checked against expected values, a flow nearly every test framework is built around from the ground up. A WebSocket connection, on the other hand, stays open persistently after the initial handshake, and messages can be sent from either side at arbitrary, unpredictable times, without a given message necessarily being a direct reply to a previous one.

This asymmetry between request and response means a test can't simply wait synchronously for a single reply, it needs to actively observe a message stream and deliberately search within it for the message relevant to the test, while extra messages irrelevant to the specific test case, say heartbeat pings, may well show up during that observation and must not be treated as an error.

Another protocol-specific aspect is ping-pong frames, which many WebSocket implementations automatically exchange in the background to mark a connection as alive and detect silent but dead connections early, where a test that accidentally waits on such a ping-pong frame as if it were an actual application message can incorrectly fail or hang if the message filter doesn't explicitly exclude these protocol frames.

2. Basic test setup with a WebSocket client library

A basic WebSocket test needs a client library that establishes a connection to the WebSocket endpoint, makes incoming messages receivable through event listeners, and allows sending its own messages, a basic structure that barely differs whether the test runs in Node.js with the ws library or directly through the browser's native WebSocket API.


import { test, expect } from '@playwright/test';
import WebSocket from 'ws';

test('WebSocket connection gets established successfully', async () => {
  const ws = new WebSocket('wss://shop.example.test/ws/stock-updates');

  await new Promise((resolve, reject) => {
    ws.on('open', resolve);
    ws.on('error', reject);
  });

  expect(ws.readyState).toBe(WebSocket.OPEN);
  ws.close();
});

3. Deliberately testing connection setup and authentication

Beyond simply proving a successful handshake, it pays off to deliberately test authentication behavior, say whether a connection without a valid token gets correctly rejected with an error code, or whether a connection with an expired token gets closed again immediately after the handshake instead of silently staying open and only failing on the first message.

A frequently overlooked test case is a connection attempt with an incorrect or missing origin domain, provided the server implements origin checking, since a misconfigured or accidentally too-permissive origin check represents a real security risk that a deliberate, automated test case can reliably monitor.

4. Checking message exchange: sending, receiving, and assertions

When testing the actual message exchange, the test sends a message over the open connection and then deliberately waits for a matching reply, where the waiting function shouldn't simply accept the next arriving message, it needs to actively check by message type or identifier whether it's actually the expected reply and not some other, unrelated message that happened to arrive in between.

For applications relying on a guaranteed order of received messages, say a continuously updated activity feed, it's worth additionally testing that several messages sent in quick succession actually arrive at the client in the order they were sent and not swapped, since asynchronous processing on the server side can under certain circumstances lead to a swapped delivery order.


function waitForMessage(ws, predicate, timeoutMs = 5000) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error('Timeout while waiting for message')), timeoutMs);
    ws.on('message', (raw) => {
      const data = JSON.parse(raw.toString());
      if (predicate(data)) {
        clearTimeout(timer);
        resolve(data);
      }
    });
  });
}

test('a stock update message is sent after a change', async () => {
  const ws = new WebSocket('wss://shop.example.test/ws/stock-updates');
  await new Promise((resolve) => ws.on('open', resolve));

  ws.send(JSON.stringify({ type: 'subscribe', sku: 'TEST-001' }));
  const update = await waitForMessage(ws, (m) => m.type === 'stock_update' && m.sku === 'TEST-001');

  expect(update.quantity).toBeGreaterThanOrEqual(0);
  ws.close();
});

5. Testing reconnect behavior after an unexpected connection drop

A realistic WebSocket frontend usually implements automatic reconnection after an unexpected connection drop, which is why deliberately testing exactly that behavior matters just as much as testing normal message flow: the test forces a connection drop by actively closing the connection server-side, then measures whether and after how long the client actually establishes a new connection, and checks whether missed messages get resent or at least the current state gets resynchronized after the reconnect.

Testing exponential backoff on repeatedly failed reconnect attempts matters especially, since a client that keeps retrying indefinitely at short, fixed intervals during a prolonged server outage would additionally burden the already struggling server with reconnect requests, instead of backing off with growing wait times.

6. Avoiding timing issues and race conditions with asynchronous messages

A common mistake in WebSocket tests is a fixed, estimated wait time between sending a message and checking the expected result, a pattern that works under favorable conditions but occasionally fails under elevated system load or network latency, because the reply hasn't arrived yet when the assertion already runs.

Instead of fixed wait times, every WebSocket test should actively wait for a concrete event, whether a specific message with matching content or a defined connection state, combined with a generous but finite timeout that only delays the test in an actual failure case and continues immediately after the expected event arrives in the normal case.

7. Intercepting WebSockets directly in Playwright browser tests

For tests reflecting the full frontend flow including WebSocket communication in the browser context, Playwright offers its own page-level WebSocket event, through which every WebSocket connection established by the browser as well as every message sent and received over it can be observed, without having to establish a second, separate connection from outside.

This approach is especially well suited to verifying that a UI update visible in the browser, say a live-updated stock level display in the Hyva frontend, actually stems from a WebSocket message received in the browser, instead of only checking the resulting DOM change while leaving the underlying WebSocket communication unchecked.


test('live stock level updates the UI via WebSocket', async ({ page }) => {
  const wsMessages = [];
  page.on('websocket', (ws) => {
    ws.on('framereceived', (frame) => wsMessages.push(JSON.parse(frame.payload)));
  });

  await page.goto('/catalog/product/view/id/123');
  await page.waitForFunction(() => wsMessages?.length > 0).catch(() => {});

  await expect(page.locator('[data-testid="stock-badge"]')).toBeVisible();
});

8. Testing multiple simultaneous connections and scaling behavior

An online store with a live stock level display or a chat feature typically needs to reliably serve not just a single connection but hundreds or thousands of concurrent WebSocket connections, which is why a deliberate load test opens multiple parallel client connections within the same test run and checks whether a broadcast message actually arrives correctly and without noticeable delay at every single connection, instead of relying on a test with only a single connection that couldn't uncover a scaling problem occurring under load at all.

Just as important as behavior under load is checking that the server actually fully cleans up closed connections instead of keeping them around in memory as so-called zombie connections, which can be demonstrated by a test that opens a defined number of connections, closes them again, and then verifies via a server metric or an internal health-check route that the active connection count returns to its original baseline afterward.

9. WebSocket test levels at a glance

The table below compares the WebSocket communication test levels presented.

Test level Tool Suited for
Direct client connection ws library in Node.js Isolated protocol and message tests
Browser-integrated Playwright page.on('websocket') Frontend behavior on real messages
Reconnect simulation Forced connection drop Reconnection logic and backoff
Load testing Multiple parallel connections Scaling behavior under many clients

Mironsoft

E2E test strategy, CI integration, and stable test suites

Test suites that actually find bugs instead of just blinking red?

We review existing E2E test suites for flakiness, missing test isolation, and inefficient CI runtimes, then build a test strategy that genuinely creates confidence instead of just checking a box.

Test Audit

Systematically uncovering flaky tests, testing pyramid gaps, and coverage blind spots.

CI Optimization

Building parallel execution, retry strategies, and fast feedback loops.

Cypress/Playwright Setup

Setting up robust E2E suites for Magento frontends from the ground up.

10. Summary

WebSocket Testing: The Essentials at a Glance

Core idea

WebSocket tests need to actively observe an ongoing message stream instead of waiting for a single response.

Strength

Connection setup, authentication, message exchange, and reconnect can each be tested deliberately and individually.

Pitfall

Fixed wait times instead of event-based waiting lead to race conditions and flakiness.

Complement

Playwright's browser-integrated WebSocket observation connects UI and protocol checking.

11. FAQ: WebSocket Testing: The Essentials at a Glance

1Why does WebSocket testing differ from HTTP testing?
Because a WebSocket connection stays open persistently and messages flow asynchronously in both directions, instead of following a fixed request-response pattern.
2How do I reliably wait for a specific WebSocket message?
By actively filtering the message stream by type or identifier, combined with a finite timeout, instead of a fixed wait time.
3How do I test reconnect behavior?
By forcing the connection closed server-side and measuring whether and how the client establishes a new connection.
4What matters when testing authentication?
Checking that invalid or expired tokens get correctly rejected, instead of silently leaving the connection open.
5Can I observe WebSockets directly in a browser test?
Yes, Playwright offers its own page-level WebSocket event for every connection the browser establishes.
6What is exponential backoff and why is it tested?
Growing wait times between reconnect attempts, so a struggling server doesn't get additionally burdened with requests.
7Are heartbeat messages a testing problem?
Not directly, but a test needs to be able to explicitly ignore them while filtering the message stream.
8How do I test multiple simultaneous WebSocket connections?
By opening multiple parallel client connections within the same test run and checking each one's message delivery.
9Do I need a real server for WebSocket tests?
A test server is enough for isolated protocol tests, frontend integration needs a running backend with a WebSocket endpoint.
10How do I avoid flakiness in WebSocket tests generally?
Through event-based waiting instead of fixed timeouts, and through unique, filterable message identifiers.