Модульное тестирование

Особенности тестирования STOMP-клиентов

STOMP.js реализует протокол обмена сообщениями поверх WebSocket или других транспортов, что делает его асинхронным и событийно-ориентированным по своей природе. Это накладывает специфические требования на модульное тестирование: необходимо учитывать отложенные события, состояние соединения и взаимодействие с внешним брокером сообщений.

Ключевая сложность тестирования заключается в том, что STOMP-клиент почти всегда зависит от внешней среды — WebSocket-соединения и брокера (RabbitMQ, ActiveMQ, Apollo и др.). Поэтому модульные тесты должны изолировать логику клиента от реального сетевого уровня.

Основные задачи тестирования STOMP.js-компонентов:

  • проверка корректности формирования STOMP-команд;
  • контроль жизненного цикла подключения;
  • обработка подписок и сообщений;
  • обработка ошибок и переподключений;
  • проверка идемпотентности действий клиента.

Изоляция WebSocket-уровня

Для полноценного модульного тестирования WebSocket-слой заменяется заглушкой (mock). Это позволяет имитировать поведение брокера без реального соединения.

Типовой подход заключается в создании фейкового WebSocket:

class MockWebSocket {
  constructor(url) {
    this.url = url;
    this.readyState = 1;
    this.sentMessages = [];
    this.listeners = {};
  }

  send(data) {
    this.sentMessages.push(data);
  }

  close() {
    this.readyState = 3;
    this.emit('close');
  }

  on(event, handler) {
    this.listeners[event] = handler;
  }

  emit(event, data) {
    if (this.listeners[event]) {
      this.listeners[event](data);
    }
  }
}

Такой подход позволяет полностью контролировать поведение транспорта и проверять только бизнес-логику STOMP-клиента.

Тестирование подключения и рукопожатия

Процесс подключения в STOMP включает отправку CONNECT и получение CONNECTED. Проверка этого сценария является базовой частью модульного тестирования.

Типовой тест проверяет:

  • отправку корректного CONNECT-фрейма;
  • обработку ответа CONNECTED;
  • установку внутреннего состояния клиента как “подключен”.

Пример тестового сценария:

test('should send CONNECT frame on connect()', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  client.connect();

  expect(ws.sentMessages[0]).toContain('CONNECT');
});

Дополнительно проверяется реакция на подтверждение соединения:

test('should set connected state on CONNECTED frame', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  client.connect();
  ws.emit('message', 'CONNECTED\nversion:1.2\n\n\u0000');

  expect(client.isConnected).toBe(true);
});

Проверка подписок (SUBSCRIBE)

Подписка является центральной частью работы STOMP-клиента. Модульные тесты должны гарантировать:

  • корректную генерацию заголовков подписки;
  • уникальность подписок;
  • корректную маршрутизацию сообщений.

Пример теста подписки:

test('should send SUBSCRIBE frame with correct destination', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  client.connect();
  client.subscribe('/queue/test', () => {});

  const lastMessage = ws.sentMessages.pop();

  expect(lastMessage).toContain('SUBSCRIBE');
  expect(lastMessage).toContain('/queue/test');
});

Также важно проверять обработку входящих сообщений:

test('should invoke callback on MESSAGE frame', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  let received = null;

  client.connect();
  client.subscribe('/queue/test', (msg) => {
    received = msg;
  });

  ws.emit('message', 'MESSAGE\ndestination:/queue/test\n\nHello\u0000');

  expect(received).toBe('Hello');
});

Тестирование отправки сообщений (SEND)

Отправка сообщений требует проверки корректности формирования STOMP-фрейма, включая заголовки и тело сообщения.

test('should send MESSAGE via SEND frame', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  client.connect();
  client.send('/queue/test', {}, 'payload');

  const frame = ws.sentMessages.find(m => m.includes('SEND'));

  expect(frame).toContain('/queue/test');
  expect(frame).toContain('payload');
});

Особое внимание уделяется сериализации payload, особенно если используется JSON:

client.send('/queue/test', {}, JSON.stringify({ a: 1 }));

В тестах проверяется, что сериализация не нарушает формат STOMP-фрейма.

Обработка ошибок и разрывов соединения

STOMP-клиент обязан корректно обрабатывать разрыв соединения и ошибки транспорта. Модульные тесты проверяют:

  • реакцию на close;
  • вызов обработчиков ошибок;
  • очистку внутренних подписок.
test('should handle connection close', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  client.connect();
  ws.close();

  expect(client.isConnected).toBe(false);
});

Также проверяется механизм повторного подключения:

test('should attempt reconnect on failure', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws, { reconnectDelay: 1000 });

  client.connect();
  ws.emit('close');

  expect(client.reconnectAttempts).toBeGreaterThan(0);
});

Тестирование очередности событий

STOMP является событийно-ориентированным протоколом, поэтому критически важно проверять порядок обработки событий.

Проверяется, что:

  • сообщения не теряются при быстром потоке;
  • обработчики вызываются в правильной последовательности;
  • подписки активируются до получения сообщений.
test('should preserve message order', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  const received = [];

  client.connect();
  client.subscribe('/queue/test', (msg) => received.push(msg));

  ws.emit('message', 'MESSAGE\ndestination:/queue/test\n\n1\u0000');
  ws.emit('message', 'MESSAGE\ndestination:/queue/test\n\n2\u0000');
  ws.emit('message', 'MESSAGE\ndestination:/queue/test\n\n3\u0000');

  expect(received).toEqual(['1', '2', '3']);
});

Моки таймеров и асинхронные сценарии

При тестировании переподключений и heartbeat часто требуется управление таймерами. Используются fake timers.

jest.useFakeTimers();

test('should send heartbeat periodically', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws, { heartbeat: 5000 });

  client.connect();

  jest.advanceTimersByTime(5000);

  expect(ws.sentMessages.some(m => m.includes('PING'))).toBe(true);
});

Асинхронные операции требуют ожидания событий:

test('should resolve after connection established', async () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  const promise = client.connectAsync();

  ws.emit('message', 'CONNECTED\nversion:1.2\n\n\u0000');

  await expect(promise).resolves.toBeUndefined();
});

Стратегии структурирования тестов

Для STOMP.js-клиентов применяются следующие подходы:

  • разделение тестов по уровням: транспорт, протокол, бизнес-логика;
  • использование фабрик клиентов для унификации настроек;
  • повторное использование mock WebSocket;
  • изоляция состояния между тестами.

Типовая структура:

tests/
  stomp/
    connection.test.js
    subscribe.test.js
    send.test.js
    reconnect.test.js
  mocks/
    MockWebSocket.js

Проверка идемпотентности операций

Некоторые операции STOMP-клиента должны быть безопасны при повторном вызове:

  • повторный connect не должен создавать дублирующее соединение;
  • повторная подписка может либо переиспользовать ID, либо заменять предыдущую.
test('should not create multiple connections', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  client.connect();
  client.connect();

  expect(ws.sentMessages.filter(m => m.includes('CONNECT')).length).toBe(1);
});

Контроль утечек подписок

При разрыве соединения необходимо убедиться, что подписки очищаются, иначе возможны утечки памяти и дублирование сообщений при переподключении.

test('should clear subscriptions on disconnect', () => {
  const ws = new MockWebSocket();
  const client = new StompClient(ws);

  client.connect();
  client.subscribe('/queue/test', () => {});

  ws.close();

  expect(client.subscriptions.size).toBe(0);
});