Мокирование клиента

Мокирование STOMP-клиента в тестовой среде опирается на подмену транспортного уровня WebSocket и поведенческой модели брокера сообщений. Основная цель заключается в воспроизведении контрактов библиотеки STOMP.js без подключения к реальному брокеру, сохраняя детерминированность сценариев.

STOMP-клиент состоит из трёх ключевых слоёв:

  • транспорт (WebSocket)
  • протокол STOMP (фреймы CONNECT, SEND, SUBSCRIBE и т.д.)
  • обработчики сообщений и подписок

Мокирование может происходить на любом из уровней, однако на практике выделяются три стратегии:

  • подмена WebSocket
  • подмена STOMP-клиента
  • имитация брокера сообщений (fake broker)

Каждая стратегия отличается глубиной контроля и сложностью реализации.

Подмена WebSocket как базовый уровень

Наиболее распространённый подход заключается в замене глобального WebSocket на фиктивную реализацию.

Базовый контракт WebSocket

Минимально необходимый интерфейс:

  • send(data)
  • close()
  • onopen
  • onmessage
  • onerror
  • onclose

Пример реализации фейкового WebSocket

class FakeWebSocket {
  constructor(url) {
    this.url = url;
    this.sent = [];
    this.readyState = 0;

    setTimeout(() => {
      this.readyState = 1;
      this.onopen && this.onopen();
    }, 0);
  }

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

  receive(data) {
    this.onmessage && this.onmessage({ data });
  }

  close() {
    this.readyState = 3;
    this.onclose && this.onclose();
  }
}

Данный слой не интерпретирует STOMP-фреймы, а лишь фиксирует поток данных.

Мокирование STOMP-клиента через Jest

Фреймворк Jest предоставляет механизм автоматической подмены модулей.

Подмена модуля @stomp/stompjs

jest.mock("@stomp/stompjs", () => {
  return {
    Client: class {
      constructor() {
        this.connected = false;
        this.subscriptions = {};
        this.published = [];
      }

      activate() {
        this.connected = true;
        this.onConnect && this.onConnect();
      }

      deactivate() {
        this.connected = false;
      }

      publish({ destination, body }) {
        this.published.push({ destination, body });
      }

      subscribe(destination, callback) {
        this.subscriptions[destination] = callback;
        return {
          unsubscribe: () => delete this.subscriptions[destination]
        };
      }
    }
  };
});

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

Имитация брокера сообщений

Более сложная модель включает эмуляцию поведения брокера:

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

FakeBroker

class FakeBroker {
  constructor() {
    this.subscriptions = new Map();
  }

  subscribe(destination, handler) {
    if (!this.subscriptions.has(destination)) {
      this.subscriptions.set(destination, new Set());
    }

    this.subscriptions.get(destination).add(handler);

    return () => {
      this.subscriptions.get(destination).delete(handler);
    };
  }

  send(destination, message) {
    const subs = this.subscriptions.get(destination);
    if (!subs) return;

    subs.forEach(handler => handler({ body: message }));
  }
}

Такой брокер позволяет тестировать сценарии fan-out доставки сообщений.

Использование адаптерного слоя

Для снижения связности тестов вводится промежуточный адаптер:

export class StompAdapter {
  constructor(client) {
    this.client = client;
  }

  connect() {
    return new Promise(resolve => {
      this.client.onConn ect = resolve;
      this.client.activate();
    });
  }

  send(destination, payload) {
    this.client.publish({
      destination,
      body: JSON.stringify(payload)
    });
  }

  subscribe(destination, handler) {
    return this.client.subscribe(destination, message => {
      handler(JSON.parse(message.body));
    });
  }
}

Мокирование осуществляется не библиотеки напрямую, а адаптера, что стабилизирует контракт.

Симуляция входящих сообщений

Ключевой элемент тестирования — имитация доставки сообщений извне.

Через FakeWebSocket

ws.receive(JSON.stringify({
  destination: "/topic/chat",
  body: JSON.stringify({ text: "hello" })
}));

Через брокер

broker.send("/topic/chat", JSON.stringify({ text: "hello" }));

Тестирование подписок

Подписка должна проверяться по двум параметрам:

  • корректность регистрации
  • корректность вызова callback
const broker = new FakeBroker();

const unsub = broker.subscribe("/topic/news", msg => {
  console.log(msg.body);
});

broker.send("/topic/news", "data");
unsub();

После unsubscribe доставка прекращается, что моделирует поведение реального STOMP.

Моделирование ошибок и нестабильности сети

Для проверки устойчивости логики вводятся сценарии отказов:

Ошибка соединения

class FaultyWebSocket extends FakeWebSocket {
  send() {
    throw new Error("Network failure");
  }
}

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

this.onclose && this.onclose({ code: 1006 });

Таймаут подключения

setTimeout(() => {
  this.onerror && this.onerror(new Error("timeout"));
}, 1000);

Проверка переподключения

Механизм переподключения зависит от состояния клиента. В моках имитируется последовательность:

  • disconnect
  • retry
  • reconnect
let attempts = 0;

client.activate = () => {
  attempts++;
  if (attempts < 3) {
    client.onDisconnect && client.onDisconnect();
  } else {
    client.onConnect && client.onConnect();
  }
};

Изоляция тестовой среды

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

  • транспортный уровень
  • бизнес-логику
  • сериализацию данных

Любая утечка реального WebSocket в тесты приводит к недетерминированным результатам.

Контроль состояния сообщений

Расширенный мок фиксирует все операции:

class TraceBroker extends FakeBroker {
  constructor() {
    super();
    this.trace = [];
  }

  send(destination, message) {
    this.trace.push({ destination, message });
    super.send(destination, message);
  }
}

Это позволяет анализировать поток сообщений без вмешательства в тестируемую систему.

Использование Sinon для управления поведением

Библиотека Sinon.js применяется для подмены методов клиента:

const stub = sinon.stub(client, "publish");

stub.callsFake(({ destination, body }) => {
  broker.send(destination, body);
});

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

Мокирование подписок с задержкой

Для имитации реальных сетевых условий добавляется асинхронность:

subscribe(destination, handler) {
  setTimeout(() => {
    this.subscriptions.set(destination, handler);
  }, 50);
}

Это позволяет тестировать race conditions.

Композиция моков

В сложных сценариях используется комбинация:

  • FakeWebSocket
  • FakeBroker
  • STOMP Adapter
  • Jest mocks

Такой стек обеспечивает полный контроль над поведением STOMP-клиента без подключения к инфраструктуре обмена сообщениями.