⚠️ Maintenance. Notice: the site is currently in maintenance mode, so the amount of available material is temporarily limited.
Home / Tutorials / TestingТестирование

Testing & load-testing WebSocket appsТестирование и нагрузочное тестирование WebSocket

Why WebSocket tests are differentЧем тесты WebSocket отличаются

A REST endpoint is a pure function of its request. A WebSocket is stateful, asynchronous and time-dependent: messages arrive out of band, order matters, and bugs hide in reconnection and timing. Good tests therefore wait for events explicitly rather than sleeping, and always tear the connection down cleanly so one test can't leak a socket into the next.

REST-эндпоинт — чистая функция от запроса. WebSocket же имеет состояние, асинхронен и зависит от времени: сообщения приходят вне очереди, порядок важен, а баги прячутся в переподключении и таймингах. Поэтому хорошие тесты явно ждут событий, а не «спят», и всегда аккуратно закрывают соединение, чтобы один тест не «утёк» сокетом в следующий.

A promise-based test helperПромис-хелпер для тестов

The single most useful utility is one that resolves on the next message (or rejects on timeout). It turns the event API into something you can await:

helpers.js
export function nextMessage(ws, timeout = 1000) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(
      () => reject(new Error("timed out waiting for message")), timeout);
    ws.addEventListener("message", (e) => {
      clearTimeout(timer);
      resolve(JSON.parse(e.data));
    }, { once: true });
  });
}

export function opened(ws) {
  return new Promise((res) => ws.addEventListener("open", res, { once: true }));
}

Самая полезная утилита — та, что резолвится на следующем сообщении (или реджектится по таймауту). Она превращает событийный API в нечто, что можно await:

helpers.js
export function nextMessage(ws, timeout = 1000) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(
      () => reject(new Error("timed out waiting for message")), timeout);
    ws.addEventListener("message", (e) => {
      clearTimeout(timer);
      resolve(JSON.parse(e.data));
    }, { once: true });
  });
}

export function opened(ws) {
  return new Promise((res) => ws.addEventListener("open", res, { once: true }));
}

Integration test against a real serverИнтеграционный тест с реальным сервером

For server logic, the most valuable test starts a real server on an ephemeral port, connects a real client, and asserts on the round-trip. Here with Node's built-in test runner:

echo.test.js
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { WebSocketServer, WebSocket } from "ws";
import { nextMessage, opened } from "./helpers.js";

const wss = new WebSocketServer({ port: 0 }); // 0 = random free port
wss.on("connection", (ws) =>
  ws.on("message", (m) => ws.send(m)));    // echo
after(() => wss.close());

test("echoes a message back", async () => {
  const { port } = wss.address();
  const ws = new WebSocket(`ws://localhost:${port}`);
  await opened(ws);

  ws.send(JSON.stringify({ hi: "there" }));
  const reply = await nextMessage(ws);

  assert.deepEqual(reply, { hi: "there" });
  ws.close();
});
Bind the server to port 0 so the OS hands you a free port — tests then run in parallel without fighting over 8080.

Для серверной логики самый ценный тест поднимает реальный сервер на временном порту, подключает реальный клиент и проверяет round-trip. Здесь — со встроенным тест-раннером Node:

echo.test.js
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { WebSocketServer, WebSocket } from "ws";
import { nextMessage, opened } from "./helpers.js";

const wss = new WebSocketServer({ port: 0 }); // 0 = случайный свободный порт
wss.on("connection", (ws) =>
  ws.on("message", (m) => ws.send(m)));    // echo
after(() => wss.close());

test("возвращает сообщение обратно", async () => {
  const { port } = wss.address();
  const ws = new WebSocket(`ws://localhost:${port}`);
  await opened(ws);

  ws.send(JSON.stringify({ hi: "there" }));
  const reply = await nextMessage(ws);

  assert.deepEqual(reply, { hi: "there" });
  ws.close();
});
Привязывайте сервер к порту 0, чтобы ОС выдала свободный порт — тогда тесты идут параллельно, не споря за 8080.

End-to-end in the browserEnd-to-end в браузере

For the full stack, Playwright drives a real browser and can observe WebSocket traffic directly, so you assert on what the user's page actually sends and receives:

chat.spec.ts
import { test, expect } from "@playwright/test";

test("sends a chat message", async ({ page }) => {
  const wsPromise = page.waitForEvent("websocket");
  await page.goto("/chat");

  const ws = await wsPromise;
  const sent = ws.waitForEvent("framesent");

  await page.fill("#input", "hello");
  await page.click("#send");

  expect((await sent).payload).toContain("hello");
});

Для всего стека Playwright управляет настоящим браузером и может напрямую наблюдать WebSocket-трафик, так что вы проверяете именно то, что страница пользователя реально шлёт и принимает:

chat.spec.ts
import { test, expect } from "@playwright/test";

test("отправляет сообщение чата", async ({ page }) => {
  const wsPromise = page.waitForEvent("websocket");
  await page.goto("/chat");

  const ws = await wsPromise;
  const sent = ws.waitForEvent("framesent");

  await page.fill("#input", "hello");
  await page.click("#send");

  expect((await sent).payload).toContain("hello");
});

Load testingНагрузочное тестирование

Functional tests prove correctness; load tests prove your server survives thousands of concurrent sockets. k6 has first-class WebSocket support and reports latency percentiles:

load.js (k6)
import ws from "k6/ws";
import { check } from "k6";

export const options = { vus: 500, duration: "30s" }; // 500 virtual users

export default function () {
  ws.connect("wss://rt.example.com/v1", {}, (socket) => {
    socket.on("open", () => socket.send("ping"));
    socket.on("message", (msg) =>
      check(msg, { "got a reply": (m) => m.length > 0 }));
    socket.setTimeout(() => socket.close(), 10000);
  });
}

For protocol-level correctness — fragmentation, control frames, UTF-8 edge cases — run the Autobahn|Testsuite against your server once; it's the conformance gold standard. Watch the heartbeat and scaling guides for the metrics that matter under load.

Функциональные тесты доказывают корректность; нагрузочные — что сервер переживает тысячи одновременных сокетов. У k6 первоклассная поддержка WebSocket и отчёт по перцентилям задержки:

load.js (k6)
import ws from "k6/ws";
import { check } from "k6";

export const options = { vus: 500, duration: "30s" }; // 500 виртуальных пользователей

export default function () {
  ws.connect("wss://rt.example.com/v1", {}, (socket) => {
    socket.on("open", () => socket.send("ping"));
    socket.on("message", (msg) =>
      check(msg, { "пришёл ответ": (m) => m.length > 0 }));
    socket.setTimeout(() => socket.close(), 10000);
  });
}

Для корректности на уровне протокола — фрагментация, управляющие кадры, краевые случаи UTF-8 — прогоните по серверу Autobahn|Testsuite; это золотой стандарт соответствия. О метриках, важных под нагрузкой, см. руководства про heartbeat и масштабирование.