Эмуляция брокера в контексте работы с STOMP.js представляет собой создание контролируемой среды, имитирующей поведение настоящего STOMP-брокера сообщений. Такая техника используется для тестирования, разработки без инфраструктурных зависимостей и воспроизведения сложных сценариев доставки сообщений без подключения к реальному серверу WebSocket/STOMP.
STOMP-протокол опирается на концепцию брокера сообщений, который принимает команды клиента и маршрутизирует сообщения между подписчиками по адресам (destinations). В типичной архитектуре брокер отвечает за:
SUBSCRIBE)SEND)ACK, NACK)Эмуляция может затрагивать любой из этих уровней, но на практике выделяются три основных подхода:
Наиболее простой вариант — подмена реального STOMP-клиента логикой, которая имитирует поведение брокера внутри процесса.
Основная идея: все подписки и сообщения хранятся в памяти.
class MockBroker {
constructor() {
this.subscriptions = new Map();
}
subscribe(destination, callback) {
if (!this.subscriptions.has(destination)) {
this.subscriptions.set(destination, new Set());
}
this.subscriptions.get(destination).add(callback);
return {
unsubscribe: () => {
this.subscriptions.get(destination)?.delete(callback);
}
};
}
send(destination, body) {
const subs = this.subscriptions.get(destination);
if (!subs) return;
const frame = {
destination,
body: typeof body === 'string' ? body : JSON.stringify(body)
};
subs.forEach(cb => cb(frame));
}
}
Такая модель полностью исключает сетевую часть и позволяет тестировать логику подписок и обработки сообщений.
При использовании STOMP.js можно подменить транспортный слой, внедрив mock через адаптер.
STOMP-клиент ожидает WebSocket-подобный интерфейс:
sendcloseonmessageЭмуляция может реализовать этот контракт:
class MockWebSocket {
constructor(broker) {
this.broker = broker;
this.onmess age = null;
}
send(frame) {
const parsed = JSON.parse(frame);
if (parsed.command === 'SUBSCRIBE') {
this.broker.subscribe(parsed.destination, (msg) => {
this.onmessage?.({
data: JSON.stringify(msg)
});
});
}
if (parsed.command === 'SEND') {
this.broker.send(parsed.destination, parsed.body);
}
}
close() {}
}
Реальный брокер поддерживает сложные правила маршрутизации:
/queue/*)/topic/*)В эмуляции важно воспроизвести хотя бы базовую семантику.
class RoutingMockBroker {
constructor() {
this.subscriptions = [];
}
subscribe(pattern, callback) {
this.subscriptions.push({ pattern, callback });
}
send(destination, message) {
this.subscriptions.forEach(sub => {
if (this.match(sub.pattern, destination)) {
sub.callback({ destination, body: message });
}
});
}
match(pattern, destination) {
if (pattern === destination) return true;
if (pattern.endsWith('*')) {
const base = pattern.slice(0, -1);
return destination.startsWith(base);
}
return false;
}
}
Для более точной симуляции поведения брокера необходимо разделять semantics:
class AdvancedMockBroker {
constructor() {
this.queues = new Map();
this.topics = new Map();
}
subscribe(destination, callback) {
const isQueue = destination.startsWith('/queue/');
const store = isQueue ? this.queues : this.topics;
if (!store.has(destination)) {
store.set(destination, new Set());
}
const set = store.get(destination);
set.add(callback);
return {
unsubscribe: () => set.delete(callback)
};
}
send(destination, message) {
const isQueue = destination.startsWith('/queue/');
const store = isQueue ? this.queues : this.topics;
const subscribers = store.get(destination);
if (!subscribers || subscribers.size === 0) return;
if (isQueue) {
const [first] = subscribers;
first({ destination, body: message });
} else {
subscribers.forEach(cb => cb({ destination, body: message }));
}
}
}
В реальном STOMP-брокере подтверждение доставки играет ключевую роль. Эмуляция может включать ручное управление состоянием сообщений.
class AckMockBroker {
constructor() {
this.subscriptions = new Map();
this.unacked = new Map();
}
subscribe(destination, callback) {
this.subscriptions.set(destination, callback);
}
send(destination, message) {
const cb = this.subscriptions.get(destination);
if (!cb) return;
const messageId = Date.now() + Math.random();
const frame = {
id: messageId,
destination,
body: message,
ack: (status) => this.ack(messageId, status)
};
this.unacked.set(messageId, frame);
cb(frame);
}
ack(id, status) {
const msg = this.unacked.get(id);
if (!msg) return;
if (status === 'ACK') {
this.unacked.delete(id);
}
if (status === 'NACK') {
this.resend(msg);
}
}
resend(msg) {
const cb = this.subscriptions.get(msg.destination);
cb?.(msg);
}
}
Эмуляция брокера применяется для проверки:
Пример тестового сценария:
test('message delivery', () => {
const broker = new AdvancedMockBroker();
let received = null;
broker.subscribe('/topic/news', (msg) => {
received = msg.body;
});
broker.send('/topic/news', 'hello');
expect(received).toBe('hello');
});
Реальные брокеры работают асинхронно, поэтому mock должен учитывать тайминги.
send(destination, message, delay = 0) {
setTimeout(() => {
const subs = this.subscriptions.get(destination);
subs?.forEach(cb => cb({ destination, body: message }));
}, delay);
}
Такой подход позволяет тестировать:
Эмуляция брокера часто включает сценарии нестабильной сети:
send(destination, message) {
if (Math.random() < 0.2) {
throw new Error('Simulated broker failure');
}
const subs = this.subscriptions.get(destination);
subs?.forEach(cb => cb({ destination, body: message }));
}
В сложных тестовых средах эмулятор брокера включает несколько слоёв:
Такая структура позволяет максимально приблизиться к поведению реального сервера без внешних зависимостей и сохраняет предсказуемость тестов при работе с STOMP.js.