Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import * as METRIC from "./src/metrics/constants";
// --- MIDDLEWARES ---

import type { CallMiddlewareHandler, Middleware } from "./src/middleware";
import * as Testing from "./src/testing";

// --- SERVICE REGISTRY ---

Expand Down Expand Up @@ -177,6 +178,7 @@ declare namespace Moleculer {
export { MetricTypes, MetricReporters, MetricRegistry, METRIC };

export { CallMiddlewareHandler, Middleware };
export { Testing };

export { Registry, Discoverers, EndpointList, Endpoint, ActionEndpoint, EventEndpoint };

Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module.exports = {
Discoverers: require("./src/registry/discoverers"),

Middlewares: require("./src/middlewares"),
Testing: require("./src/testing"),

Errors: require("./src/errors"),

Expand Down
1 change: 1 addition & 0 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const Strategies = mod.Strategies;
export const TracerExporters = mod.TracerExporters;
export const Transit = mod.Transit;
export const Transporters = mod.Transporters;
export const Testing = mod.Testing;
export const Utils = mod.Utils;
export const Validator = mod.Validator;
export const Validators = mod.Validators;
107 changes: 107 additions & 0 deletions src/testing/event-catcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* moleculer
* Copyright (c) 2026 MoleculerJS (https://github.com/moleculerjs/moleculer)
* MIT Licensed
*/

"use strict";

const { isFunction } = require("../utils");

function normalizeOptions(opts) {
if (typeof opts === "number") return { timeout: opts };
return opts || {};
}

module.exports = function EventCatcherMiddleware(broker) {
const events = [];
const waiters = [];

function matches(expected, actual) {
if (expected == null) return true;
if (isFunction(expected)) return expected(actual);
if (expected instanceof RegExp) return expected.test(actual.name);
return expected === actual.name;
}

function resolveWaiters(event) {
for (let i = waiters.length - 1; i >= 0; i--) {
const waiter = waiters[i];
if (matches(waiter.expected, event)) {
clearTimeout(waiter.timer);
waiters.splice(i, 1);
waiter.resolve(event);
}
}
}

function record(name, payload, opts, type) {
const event = {
name,
payload,
opts,
type,
timestamp: Date.now()
};

events.push(event);
resolveWaiters(event);
}

broker.events = {
getEvents() {
return events.slice();
},

clear() {
events.length = 0;
},

find(expected) {
return events.find(event => matches(expected, event));
},

waitFor(expected, opts) {
opts = normalizeOptions(opts);
const timeout = opts.timeout == null ? 1000 : opts.timeout;
const existing = events.find(event => matches(expected, event));

if (existing) return broker.Promise.resolve(existing);

return new broker.Promise((resolve, reject) => {
const waiter = { expected, resolve };
waiter.timer = setTimeout(() => {
const idx = waiters.indexOf(waiter);
if (idx !== -1) waiters.splice(idx, 1);
reject(new Error(`Event '${expected}' was not emitted within ${timeout}ms.`));
}, timeout);
waiters.push(waiter);
});
}
};

return {
name: "EventCatcher",

emit(next) {
return (eventName, payload, opts) => {
record(eventName, payload, opts, "emit");
return next(eventName, payload, opts);
};
},

broadcast(next) {
return (eventName, payload, opts) => {
record(eventName, payload, opts, "broadcast");
return next(eventName, payload, opts);
};
},

broadcastLocal(next) {
return (eventName, payload, opts) => {
record(eventName, payload, opts, "broadcastLocal");
return next(eventName, payload, opts);
};
}
};
};
42 changes: 42 additions & 0 deletions src/testing/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { BrokerOptions, CallingOptions } from "../service-broker";
import type ServiceBroker = require("../service-broker");
import type { ServiceSchema } from "../service";
import type { Middleware } from "../middleware";

export interface CaughtEvent<TPayload = any> {
name: string;
payload: TPayload;
opts?: Record<string, any>;
type: "emit" | "broadcast" | "broadcastLocal";
timestamp: number;
}

export interface TestingBroker extends ServiceBroker {
events: {
getEvents(): CaughtEvent[];
clear(): void;
find(
expected?: string | RegExp | ((event: CaughtEvent) => boolean)
): CaughtEvent | undefined;
waitFor(
expected?: string | RegExp | ((event: CaughtEvent) => boolean),
opts?: number | { timeout?: number }
): Promise<CaughtEvent>;
};
mockAction(actionName: string, handler: Function | any): this;
clearActionMocks(): this;
getMockedActionCalls(actionName?: string): Array<{
actionName: string;
params: any;
opts: CallingOptions;
result?: any;
}>;
}

export interface TestBrokerOptions extends BrokerOptions {
mockServices?: ServiceSchema[] | Record<string, Partial<ServiceSchema>>;
}

export declare function createBroker(opts?: TestBrokerOptions): TestingBroker;
export declare function EventCatcher(broker: ServiceBroker): Middleware;
export declare function MockingCalls(broker: ServiceBroker): Middleware;
47 changes: 47 additions & 0 deletions src/testing/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* moleculer
* Copyright (c) 2026 MoleculerJS (https://github.com/moleculerjs/moleculer)
* MIT Licensed
*/

"use strict";

const _ = require("lodash");

const ServiceBroker = require("../service-broker");
const EventCatcher = require("./event-catcher");
const MockingCalls = require("./mocking-calls");

function toServiceSchemas(mockServices) {
if (!mockServices) return [];
if (Array.isArray(mockServices)) return mockServices;

return Object.keys(mockServices).map(name => {
const service = mockServices[name];
return {
name,
...service
};
});
}

function createBroker(opts = {}) {
const { mockServices, middlewares, ...brokerOptions } = opts;
const broker = new ServiceBroker(
_.defaultsDeep({}, brokerOptions, {
logger: false,
test: true,
middlewares: [EventCatcher, MockingCalls].concat(middlewares || [])
})
);

toServiceSchemas(mockServices).forEach(schema => broker.createService(schema));

return broker;
}

module.exports = {
createBroker,
EventCatcher,
MockingCalls
};
55 changes: 55 additions & 0 deletions src/testing/mocking-calls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* moleculer
* Copyright (c) 2026 MoleculerJS (https://github.com/moleculerjs/moleculer)
* MIT Licensed
*/

"use strict";

const { isFunction } = require("../utils");

module.exports = function MockingCallsMiddleware(broker) {
const mocks = new Map();
const calls = [];

function normalizeMock(handler) {
if (isFunction(handler)) return handler;
return () => handler;
}

broker.mockAction = function mockAction(actionName, handler) {
mocks.set(actionName, normalizeMock(handler));
return broker;
};

broker.clearActionMocks = function clearActionMocks() {
mocks.clear();
calls.length = 0;
return broker;
};

broker.getMockedActionCalls = function getMockedActionCalls(actionName) {
const selected = actionName ? calls.filter(call => call.actionName === actionName) : calls;
return selected.slice();
};

return {
name: "MockingCalls",

call(next) {
return (actionName, params, opts = {}) => {
if (!mocks.has(actionName)) return next(actionName, params, opts);

const call = { actionName, params, opts };
calls.push(call);

return broker.Promise.resolve()
.then(() => mocks.get(actionName).call(broker, params, opts, call))
.then(result => {
call.result = result;
return result;
});
};
}
};
};
Loading