Мокирование STOMP-клиента в тестовой среде опирается на подмену транспортного уровня WebSocket и поведенческой модели брокера сообщений. Основная цель заключается в воспроизведении контрактов библиотеки STOMP.js без подключения к реальному брокеру, сохраняя детерминированность сценариев.
STOMP-клиент состоит из трёх ключевых слоёв:
Мокирование может происходить на любом из уровней, однако на практике выделяются три стратегии:
Каждая стратегия отличается глубиной контроля и сложностью реализации.
Наиболее распространённый подход заключается в замене глобального WebSocket на фиктивную реализацию.
Минимально необходимый интерфейс:
send(data)close()onopenonmessageonerroroncloseclass 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-фреймы, а лишь фиксирует поток данных.
Фреймворк Jest предоставляет механизм автоматической подмены модулей.
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]
};
}
}
};
});
Такой подход позволяет тестировать бизнес-логику, не взаимодействуя с реальным транспортом.
Более сложная модель включает эмуляцию поведения брокера:
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));
});
}
}
Мокирование осуществляется не библиотеки напрямую, а адаптера, что стабилизирует контракт.
Ключевой элемент тестирования — имитация доставки сообщений извне.
ws.receive(JSON.stringify({
destination: "/topic/chat",
body: JSON.stringify({ text: "hello" })
}));
broker.send("/topic/chat", JSON.stringify({ text: "hello" }));
Подписка должна проверяться по двум параметрам:
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);
Механизм переподключения зависит от состояния клиента. В моках имитируется последовательность:
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.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.
В сложных сценариях используется комбинация:
Такой стек обеспечивает полный контроль над поведением STOMP-клиента без подключения к инфраструктуре обмена сообщениями.