Тестирование ошибок

Ошибки в STOMP-клиентах не являются побочным эффектом — они являются частью нормального жизненного цикла соединения. В STOMP.js ошибки возникают на разных уровнях: транспортном, протокольном и прикладном. Для корректного тестирования необходимо разделять эти уровни, поскольку поведение приложения зависит от типа сбоя, а не от факта ошибки как таковой.

Тестирование ошибок в STOMP.js строится вокруг нескольких ключевых сценариев:

  • потеря соединения с брокером
  • отказ аутентификации
  • некорректные STOMP-команды
  • ошибки подписки и доставки сообщений
  • таймауты и сетевые сбои

Каждый сценарий требует отдельной стратегии эмуляции и проверки.


Структура обработки ошибок в STOMP.js

STOMP.js предоставляет несколько уровней обработки ошибок:

  1. Callback onStompError
  2. Callback onWebSocketError
  3. Callback onDisconnect
  4. Promise/async ошибки при подключении (в новых реализациях)
  5. Ошибки внутри frame-команд

Типичная конфигурация клиента:

import { Client } from '@stomp/stompjs';

const client = new Client({
  brokerURL: 'ws://localhost:15674/ws',
  reconnectDelay: 0,
  debug: (msg) => console.log(msg),

  onStompError: (frame) => {
    console.error('STOMP error:', frame.headers['message']);
  },

  onWebSocketError: (event) => {
    console.error('WebSocket error:', event);
  },

  onDisconnect: (frame) => {
    console.warn('Disconnected:', frame);
  }
});

При тестировании важно не просто проверить факт вызова callback, но и гарантировать корректность входных данных frame, event и состояния клиента.


Подход к тестированию ошибок через мокирование WebSocket

Основной способ тестирования ошибок — замена WebSocket на контролируемый mock.

class MockWebSocket {
  constructor(url) {
    this.url = url;
    this.readyState = 0;
    setTimeout(() => this.onopen?.({}), 10);
  }

  send(data) {
    this.lastSent = data;
  }

  close() {
    this.readyState = 3;
    this.onclose?.({ code: 1006, reason: 'abnormal' });
  }

  simulateError() {
    this.onerror?.(new Event('error'));
  }
}

Подмена в STOMP.js:

global.WebSocket = MockWebSocket;

Далее можно тестировать поведение клиента при различных сбоях:

  • вызов onWebSocketError
  • переход состояния клиента в CLOSED
  • отсутствие повторного подключения при reconnectDelay = 0

Тестирование STOMP error frame (ошибки брокера)

STOMP протокол определяет отдельный тип frame — ERROR. Он приходит от брокера и не связан напрямую с WebSocket.

Пример тестовой симуляции:

function simulateStompError(client) {
  const frame = {
    command: 'ERROR',
    headers: {
      message: 'Authentication failed'
    },
    body: 'Bad credentials'
  };

  client.onStompError(frame);
}

В тестах важно проверять:

  • корректность разбора headers.message
  • доступность frame.body
  • отсутствие падения клиента
  • сохранение соединения или его корректное закрытие

Тестирование ошибки аутентификации

Ошибка аутентификации является частным случаем ERROR frame, но требует отдельного сценария, поскольку влияет на весь жизненный цикл подключения.

test('auth error handling', () => {
  const client = createClient();

  let errorReceived = null;

  client.onStompEr ror = (frame) => {
    errorReceived = frame;
  };

  simulateStompError(client);

  expect(errorReceived.headers.message).toBe('Authentication failed');
});

Критически важно проверять не только факт ошибки, но и то, что клиент не пытается автоматически переподключиться при фатальной причине.


Тестирование разрыва соединения

Разрыв соединения может быть инициирован как сервером, так и сетью.

Сценарий тестирования:

test('unexpected disconnect', () => {
  const client = createClient();

  let disconnected = false;

  client.onDisconn ect = () => {
    disconnected = true;
  };

  client.activate();

  client.webSocket.close();

  expect(disconnected).toBe(true);
});

Особое внимание уделяется состояниям:

  • CONNECTING
  • OPEN
  • DISCONNECTING
  • CLOSED

Неправильный переход состояний является частой причиной утечек подписок.


Тестирование переподключения при ошибках

STOMP.js поддерживает автоматическое переподключение через reconnectDelay.

const client = new Client({
  reconnectDelay: 2000
});

Тест должен проверять:

  • факт повторного вызова activate
  • сохранение подписок
  • отсутствие дублирования сообщений

Мокирование таймеров:

jest.useFakeTimers();

client.activate();

client.webSocket.simulateError();

jest.advanceTimersByTime(2000);

expect(client.connected).toBe(true);

Ошибки подписок и доставки сообщений

Ошибки могут возникать не только при соединении, но и при подписке.

client.subscribe('/topic/test', (message) => {
  throw new Error('Handler failure');
});

Хотя STOMP.js не обязан обрабатывать исключения внутри callback, тестирование должно учитывать:

  • изоляцию обработчиков сообщений
  • отсутствие остановки доставки сообщений
  • логирование ошибок

Тест:

test('subscription handler error isolation', () => {
  const client = createClient();

  client.subscribe('/topic/test', () => {
    throw new Error('fail');
  });

  expect(() => {
    client._deliverMessage({
      body: 'data'
    });
  }).not.toThrow();
});

Тестирование сетевых таймаутов

Сетевые таймауты не всегда явно поддерживаются WebSocket API, поэтому их моделирование осуществляется через искусственные задержки.

class SlowWebSocket extends MockWebSocket {
  send() {
    setTimeout(() => {
      this.onerror?.(new Event('timeout'));
    }, 5000);
  }
}

Проверяются следующие аспекты:

  • переход клиента в состояние ошибки
  • корректная отмена pending операций
  • отсутствие зависших подписок

Проверка устойчивости клиента к множественным ошибкам

Реальные системы часто генерируют каскад ошибок: разрыв соединения + ошибка брокера + сбой подписки.

Тестирование должно учитывать:

  • идемпотентность обработки ошибок
  • защиту от повторных вызовов callback
  • отсутствие гонок состояния
test('error storm handling', () => {
  const client = createClient();

  let errorCount = 0;

  client.onWebSocketEr ror = () => errorCount++;
  client.onStompEr ror = () => errorCount++;

  client.webSocket.simulateError();
  simulateStompError(client);

  expect(errorCount).toBe(2);
});

Изоляция состояния клиента при ошибках

Одним из ключевых аспектов тестирования является контроль состояния клиента после ошибки.

Проверяются:

  • connected === false
  • отсутствие активных подписок
  • очищенные таймеры
  • корректное поведение deactivate()
test('state cleanup after error', () => {
  const client = createClient();

  client.activate();
  client.webSocket.simulateError();

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

Логирование и трассировка ошибок в тестовой среде

В тестах часто требуется не только проверка результата, но и анализ логов STOMP.js.

const logs = [];

const client = new Client({
  debug: (msg) => logs.push(msg)
});

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

  • наличие ошибок в логах
  • отсутствие лишних reconnect-циклов
  • последовательность событий

Контракт ошибок между STOMP-клиентом и приложением

Ошибки STOMP.js должны рассматриваться как часть контракта API:

  • WebSocket error → техническая ошибка транспорта
  • STOMP ERROR frame → логическая ошибка брокера
  • disconnect → состояние соединения
  • timeout → нарушение SLA

Тестирование строится вокруг проверки соблюдения этого контракта, а не только факта вызова callback-функций.