tests.ws

How to Test WebSocket Connections

websocket testing devtools automation

Testing WebSocket connections is different from testing HTTP APIs. Connections are stateful, messages flow in both directions, and timing matters. You need different tools and approaches than standard API testing. A broken WebSocket endpoint can silently fail, dropping messages or leaking connections without returning the clear error codes you get from REST endpoints.

This guide covers every layer of WebSocket testing, from quick manual checks in your browser to fully automated test suites running in CI.

What to Test

Before picking tools, define what you actually need to verify. WebSocket testing covers several areas that HTTP testing does not.

Connection lifecycle. Every WebSocket connection moves through distinct states: opening handshake, open (sending and receiving messages), closing handshake, and closed. A proper WebSocket connection test should verify behavior at each stage. Does the server accept the connection? Does it respond to a close frame with its own close frame? Does it send the correct close code?

Message format validation. If your server expects JSON, what happens when a client sends plain text? What about malformed JSON? Your server should handle bad input gracefully, not crash.

Authentication flow. Many WebSocket servers require authentication during the handshake (via cookies, tokens in query params, or an auth message after connecting). Test that valid credentials succeed and invalid credentials fail with the right error.

Reconnection behavior. Clients often implement automatic reconnection. Test that your client reconnects after a dropped connection, backs off appropriately, and re-authenticates if needed.

Edge cases. These include large messages (what is your size limit?), binary data (Blob vs ArrayBuffer), and concurrent connections from the same client. Edge cases reveal the bugs your users will find in production.

Testing with Browser DevTools

The fastest way to inspect a live WebSocket connection is through your browser’s developer tools.

Chrome DevTools

  1. Open DevTools (F12 or Cmd+Option+I on macOS).
  2. Go to the Network tab.
  3. Click the WS filter to show only WebSocket connections.
  4. Load the page or trigger the WebSocket connection.
  5. Click on the WebSocket entry in the list.
  6. Select the Messages sub-tab.

The Messages panel shows every frame sent and received. Outgoing messages have a green arrow pointing up. Incoming messages have a red arrow pointing down. Each entry shows the data payload, the length in bytes, and the timestamp.

You can click any message to see its full content. For JSON messages, Chrome formats them for readability.

Firefox DevTools

Firefox provides a similar workflow. Open DevTools, go to the Network tab, and filter by “WS.” Click a WebSocket connection, then select the Messages panel. Firefox also color-codes sent and received frames.

Reading Frames and Close Codes

Pay attention to the final frames in the connection. The close frame contains a numeric close code and an optional reason string. Common codes include:

  • 1000 - Normal closure
  • 1001 - Going away (server shutting down or page navigating)
  • 1006 - Abnormal closure (no close frame received)
  • 1008 - Policy violation
  • 1011 - Internal server error

If you see 1006, the connection dropped without a proper close handshake. This usually means a network interruption or a server crash. For a full reference, see the close codes guide.

Testing with Command-Line Tools

Browser DevTools work well for manual inspection, but command-line tools give you more control and are scriptable.

wscat

wscat is a simple WebSocket client for your terminal. Install it with npm:

npm install -g wscat

Connect to a WebSocket server:

wscat -c ws://localhost:8080

Once connected, type messages and press Enter to send them. The tool prints incoming messages to stdout.

You can pass headers for authentication:

wscat -c ws://localhost:8080 \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."

Send a single message and disconnect:

echo '{"type":"ping"}' | wscat -c ws://localhost:8080 -w 2

The -w 2 flag sets a 2-second wait timeout before closing.

websocat

websocat is a more powerful alternative. It supports piping, scripting, and protocol-level control.

Install on macOS:

brew install websocat

Connect and interact:

websocat ws://localhost:8080

Pipe messages from a file:

cat messages.txt | websocat ws://localhost:8080

websocat also supports binary frames, custom close codes, and connecting two WebSocket endpoints together. It is the better choice for scripted testing.

curl (Handshake Only)

curl cannot maintain a WebSocket connection, but it can verify the HTTP upgrade handshake:

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGVzdA==" \
  http://localhost:8080

A successful response returns HTTP 101 Switching Protocols. This confirms the server accepts WebSocket connections at that endpoint, even though curl cannot proceed with the framing protocol.

Testing with Online Tools

Sometimes you need to test a WebSocket server quickly without installing anything. Browser-based WebSocket testers let you connect, send messages, and view responses from a web page.

The WebSocket Tester on this site connects to any accessible WebSocket endpoint. You can use it to test WebSocket online from your browser without any installation. Enter the URL, click connect, and start sending messages. The tool displays each frame with timestamps, direction, and payload size.

Online tools work best for:

  • Quick smoke tests against a staging server
  • Sharing a testing workflow with teammates who do not have CLI tools installed
  • Testing from a different network or region

For repeated testing, scripted scenarios, or CI pipelines, command-line tools are the better fit.

Testing with Browser Extensions

Browser extensions sit between your web application and the WebSocket server, giving you visibility and control that DevTools alone cannot provide.

The tests.ws Chrome extension intercepts WebSocket traffic in any tab. You can inspect messages in real time, modify outgoing frames before they reach the server, and replay previous messages to reproduce bugs. This is especially useful when debugging WebSocket issues in production environments where you cannot attach a proxy.

Extensions offer several advantages over DevTools:

  • Message modification. Change the payload of an outgoing message to test how the server handles unexpected data.
  • Replay. Re-send a previous message without reloading the page or re-triggering the user action.
  • Filtering. Hide noisy heartbeat messages and focus on the frames you care about.
  • Persistence. Keep a log of WebSocket traffic across page reloads.

Unit Testing WebSocket Servers

Manual testing catches obvious problems, but automated tests prevent regressions. Here is a full test suite for a Node.js WebSocket server using the ws library and Vitest.

First, a minimal server to test:

// server.js
import { WebSocketServer } from "ws";

export function createServer(port) {
  const wss = new WebSocketServer({ port });

  wss.on("connection", (ws, req) => {
    const token = new URL(
      req.url,
      "http://localhost"
    ).searchParams.get("token");

    if (token !== "valid-token") {
      ws.close(1008, "Invalid token");
      return;
    }

    ws.on("message", (data) => {
      const message = JSON.parse(data);

      if (message.type === "echo") {
        ws.send(JSON.stringify({
          type: "echo",
          payload: message.payload,
        }));
      }

      if (message.type === "broadcast") {
        wss.clients.forEach((client) => {
          if (client.readyState === 1) {
            client.send(JSON.stringify({
              type: "broadcast",
              payload: message.payload,
            }));
          }
        });
      }
    });
  });

  return wss;
}

Now the test suite:

// server.test.js
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import WebSocket from "ws";
import { createServer } from "./server.js";

let wss;
const PORT = 9876;

beforeAll(() => {
  wss = createServer(PORT);
});

afterAll(() => {
  wss.close();
});

function connectClient(token = "valid-token") {
  return new Promise((resolve, reject) => {
    const ws = new WebSocket(
      `ws://localhost:${PORT}?token=${token}`
    );
    ws.on("open", () => resolve(ws));
    ws.on("error", reject);
  });
}

function waitForMessage(ws) {
  return new Promise((resolve) => {
    ws.once("message", (data) => {
      resolve(JSON.parse(data));
    });
  });
}

function waitForClose(ws) {
  return new Promise((resolve) => {
    ws.on("close", (code, reason) => {
      resolve({ code, reason: reason.toString() });
    });
  });
}

describe("WebSocket Server", () => {
  it("accepts connections with valid token", async () => {
    const ws = await connectClient("valid-token");
    expect(ws.readyState).toBe(WebSocket.OPEN);
    ws.close();
  });

  it("rejects connections with invalid token", async () => {
    const ws = new WebSocket(
      `ws://localhost:${PORT}?token=bad-token`
    );
    const { code, reason } = await waitForClose(ws);
    expect(code).toBe(1008);
    expect(reason).toBe("Invalid token");
  });

  it("echoes messages back to sender", async () => {
    const ws = await connectClient();
    const messagePromise = waitForMessage(ws);

    ws.send(JSON.stringify({
      type: "echo",
      payload: "hello",
    }));

    const response = await messagePromise;
    expect(response.type).toBe("echo");
    expect(response.payload).toBe("hello");
    ws.close();
  });

  it("broadcasts messages to all clients", async () => {
    const ws1 = await connectClient();
    const ws2 = await connectClient();

    const msg1 = waitForMessage(ws1);
    const msg2 = waitForMessage(ws2);

    ws1.send(JSON.stringify({
      type: "broadcast",
      payload: "everyone",
    }));

    const [response1, response2] = await Promise.all([
      msg1,
      msg2,
    ]);

    expect(response1.payload).toBe("everyone");
    expect(response2.payload).toBe("everyone");

    ws1.close();
    ws2.close();
  });
});

Run the tests:

npx vitest run server.test.js

Each test creates its own client connections and cleans them up. The waitForMessage helper wraps the event-based API in a Promise so you can use async/await for cleaner test code.

Integration Testing

Unit tests verify individual behaviors. Integration tests verify the full flow: authenticate over HTTP, establish a WebSocket connection, exchange messages, and disconnect cleanly.

it("full authentication and messaging flow", async () => {
  // Step 1: Get auth token via HTTP
  const response = await fetch(
    "http://localhost:3000/api/auth",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        username: "testuser",
        password: "testpass",
      }),
    }
  );
  const { token } = await response.json();

  // Step 2: Connect WebSocket with token
  const ws = new WebSocket(
    `ws://localhost:3000/ws?token=${token}`
  );

  await new Promise((resolve) => ws.on("open", resolve));

  // Step 3: Send a message and verify response
  const responsePromise = waitForMessage(ws);
  ws.send(JSON.stringify({ type: "ping" }));
  const msg = await responsePromise;
  expect(msg.type).toBe("pong");

  // Step 4: Clean disconnect
  ws.close(1000, "Test complete");
  const { code } = await waitForClose(ws);
  expect(code).toBe(1000);
});

Handling async timing. WebSocket tests are inherently asynchronous. Always set timeouts on your message expectations. If a server never responds, your test should fail with a timeout error rather than hanging forever:

function waitForMessage(ws, timeoutMs = 5000) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error(
        `No message received within ${timeoutMs}ms`
      ));
    }, timeoutMs);

    ws.once("message", (data) => {
      clearTimeout(timer);
      resolve(JSON.parse(data));
    });
  });
}

Testing Edge Cases

Edge case testing separates a working WebSocket implementation from a production-ready one.

Invalid frames. Send a text frame to a server that expects binary, or send binary when text is expected. Verify the server responds with a proper error and does not crash the process.

Connection drop mid-message. Kill the TCP connection while a large message is in transit. The server should detect the broken connection and clean up resources. Test this by destroying the underlying socket:

ws._socket.destroy();

Then verify the server removes the client from its connection pool and does not leak memory.

Payload size limits. Most servers enforce a maximum message size. Send a message just under the limit (should succeed) and one just over it (should fail with close code 1009). For a reference on all close codes and their meanings, check the WebSocket testing tools guide.

it("rejects messages exceeding size limit", async () => {
  const ws = await connectClient();
  const closePromise = waitForClose(ws);

  // Send a 2MB message to a server with 1MB limit
  const largePayload = "x".repeat(2 * 1024 * 1024);
  ws.send(largePayload);

  const { code } = await closePromise;
  expect(code).toBe(1009);
});

Rate limiting. If your server rate-limits messages, send a burst of messages and verify the server throttles or disconnects the client after exceeding the limit.

Concurrent connections. Open 100 connections from the same IP. Does the server handle them all? Does it enforce a per-IP connection limit? Does performance degrade gracefully?

Automated Testing in CI

Running WebSocket tests in a CI pipeline requires a few adjustments.

Start the server in the test process. Do not rely on an external server. Spin up the WebSocket server in a beforeAll hook and tear it down in afterAll. This makes tests self-contained and reproducible.

Use dynamic port allocation. Hardcoded ports cause conflicts when multiple test suites run in parallel. Pass port 0 to let the OS assign an available port:

const wss = new WebSocketServer({ port: 0 });
const port = wss.address().port;

Set generous timeouts. CI machines are often slower than your development laptop. Increase test timeouts to account for this:

// vitest.config.js
export default {
  test: {
    testTimeout: 15000,
  },
};

Clean up connections. Leaked connections cause the Node.js process to hang after tests finish. Track all open connections and force-close them in afterAll:

afterAll(() => {
  wss.clients.forEach((client) => {
    client.terminate();
  });
  wss.close();
});

Run tests in sequence if needed. If your tests share server state (a chat room, for example), run them serially to avoid race conditions. In Vitest, use describe.sequential or set --sequence in your config.