Testing
Unit-test services and handlers without a live broker using MockKubeMQClient, MockKubeMQServer, and KubeMQModule.forTest().
Unit tests should not depend on a running KubeMQ broker. The @kubemq/nestjs-transport/testing entry point provides in-memory mocks that let you exercise the services that send messages and the handlers that process them entirely in isolation — no network, no broker, no flakiness. The subpath exports two classes:
| Export | Replaces | Use for |
|---|---|---|
MockKubeMQClient | KubeMQClientProxy | Services that call client.send() / client.emit() |
MockKubeMQServer | KubeMQServer | Handler logic invoked through dispatch* methods |
MockKubeMQClient extends the NestJS ClientProxy, so it is a drop-in replacement anywhere a client proxy is injected. MockKubeMQServer records the handlers you register and drives them through the same Observable execution path the real transport uses, including the auto-ack and timeout behavior.
These mocks are designed for fast, broker-free unit tests. When you need true end-to-end verification against a real broker, see Integration Testing below.
Prerequisites
@kubemq/nestjs-transportinstalled, with services and handlers already built againstKubeMQModule/KubeMQServer(see Getting Started with NestJS)- No broker needed for the mock-based unit tests below; a running broker is only required for Integration Testing
Testing Services with MockKubeMQClient
MockKubeMQClient records every outbound call and returns the responses you stub. The workflow is:
setResponse('pattern', value)to stub the value asend()for that pattern resolves to (orsetError('pattern', err)to make it reject).- Run the code under test.
- Assert on the recorded
sendCalls/emitCallsarrays — each entry is{ pattern, data }. reset()between tests to clear recorded calls and stubbed responses.
Take a service that sends a command and emits an event:
import { Injectable, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class OrderService {
constructor(@Inject('KUBEMQ_SERVICE') private readonly client: ClientProxy) {}
async createOrder(product: string, quantity: number): Promise<{ orderId: string }> {
await this.client.connect();
return firstValueFrom(
this.client.send<{ orderId: string }>('nestjs-testing.orders.create', { product, quantity }),
);
}
async notifyOrderShipped(orderId: string): Promise<void> {
await this.client.connect();
await firstValueFrom(
this.client.emit('nestjs-testing.orders.shipped', { orderId }),
);
}
}Provide the mock under the same injection token the service expects and assert on what it recorded:
import { Test } from '@nestjs/testing';
import { MockKubeMQClient } from '@kubemq/nestjs-transport/testing';
import { OrderService } from './order.service.js';
describe('MockKubeMQClient', () => {
let orderService: OrderService;
let mockClient: MockKubeMQClient;
beforeEach(async () => {
mockClient = new MockKubeMQClient();
const module = await Test.createTestingModule({
providers: [
OrderService,
{ provide: 'KUBEMQ_SERVICE', useValue: mockClient },
],
}).compile();
orderService = module.get(OrderService);
});
afterEach(() => {
mockClient.reset();
});
it('should send a command and receive a mocked response', async () => {
mockClient.setResponse('nestjs-testing.orders.create', { orderId: 'ORD-001' });
const result = await orderService.createOrder('WIDGET-42', 3);
expect(result).toEqual({ orderId: 'ORD-001' });
expect(mockClient.sendCalls).toHaveLength(1);
expect(mockClient.sendCalls[0]).toEqual({
pattern: 'nestjs-testing.orders.create',
data: { product: 'WIDGET-42', quantity: 3 },
});
});
it('should emit an event and record the call', async () => {
await orderService.notifyOrderShipped('ORD-001');
expect(mockClient.emitCalls).toHaveLength(1);
expect(mockClient.emitCalls[0]).toEqual({
pattern: 'nestjs-testing.orders.shipped',
data: { orderId: 'ORD-001' },
});
});
it('should simulate an error response', async () => {
mockClient.setError('nestjs-testing.orders.create', new Error('Broker unavailable'));
await expect(orderService.createOrder('WIDGET-42', 1)).rejects.toThrow('Broker unavailable');
});
});sendCalls and emitCalls are tracked separately. client.send() (Commands and Queries) is recorded in sendCalls; client.emit() (Events, Events Store, and Queues) is recorded in emitCalls. Assert on the right array for the pattern you are testing.
Wiring the Mock via Dependency Injection
There are two ways to put MockKubeMQClient into the DI container.
Override the token explicitly with useValue. This is the most direct approach when you already construct the mock in the test and want full control over its lifecycle:
import { Test } from '@nestjs/testing';
import { MockKubeMQClient } from '@kubemq/nestjs-transport/testing';
import { OrderService } from './order.service.js';
const mockClient = new MockKubeMQClient();
const module = await Test.createTestingModule({
providers: [
OrderService,
{ provide: 'KUBEMQ_SERVICE', useValue: mockClient },
],
}).compile();KubeMQModule.forTest() registers a MockKubeMQClient (and a MockKubeMQServer) under the module system for you. Import it instead of forRoot / register, then resolve the mocks from the module:
import { Test } from '@nestjs/testing';
import { KubeMQModule } from '@kubemq/nestjs-transport';
import { MockKubeMQClient, MockKubeMQServer } from '@kubemq/nestjs-transport/testing';
import { OrderService } from './order.service.js';
const module = await Test.createTestingModule({
imports: [KubeMQModule.forTest({ name: 'KUBEMQ_SERVICE', isGlobal: true })],
providers: [OrderService],
}).compile();
const orderService = module.get(OrderService);
const mockClient = module.get(MockKubeMQClient);
const mockServer = module.get(MockKubeMQServer);The same mock instance is registered under the name token, under MockKubeMQClient, and under the internal SDK client token, so a service that injects 'KUBEMQ_SERVICE' and a test that resolves MockKubeMQClient share one object.
forTest options
KubeMQModule.forTest({ name: 'KUBEMQ_SERVICE', isGlobal: true })| Parameter | Type | Default | Description |
|---|---|---|---|
name | string | symbol | 'KUBEMQ_SERVICE' | Injection token the mock client is registered under |
isGlobal | boolean | false | Register the test module as a global module |
Pass the same name you used in your production KubeMQModule.register({ name }) so the service under test resolves the mock without any code changes. Both arguments are optional — calling KubeMQModule.forTest() with no options registers the mock under the default 'KUBEMQ_SERVICE'.
Testing Handlers with MockKubeMQServer
MockKubeMQServer tests the handler side. Register a handler for a pattern with addHandler('pattern', fn), then drive it with one of the dispatch methods:
| Method | Drives | Returns |
|---|---|---|
dispatchCommand(pattern, data) | A command handler | { executed, response?, error? } |
dispatchQuery(pattern, data) | A query handler | { executed, response?, error? } |
dispatchEvent(pattern, data) | An event handler | void |
dispatchEventStore(pattern, data, sequence?) | An events store handler | void |
dispatchQueueMessage(pattern, data) | A queue handler | { acked, reQueued? } |
Each dispatch builds a real context object, converts the handler result to an Observable, and resolves it the same way the live transport does. For request-reply, executed is true when the handler returned a value and false when no handler is registered or the handler threw — in which case error carries the message. For queues, the mock auto-acks when the handler completes without explicitly calling ack() / nack() / reQueue().
export function handleCreateOrder(data: { product: string; quantity: number }): {
orderId: string;
product: string;
status: string;
} {
return {
orderId: `ORD-${Date.now()}`,
product: data.product,
status: 'created',
};
}import { MockKubeMQServer } from '@kubemq/nestjs-transport/testing';
import { handleCreateOrder } from './order.handler.js';
describe('MockKubeMQServer', () => {
let server: MockKubeMQServer;
beforeEach(() => {
server = new MockKubeMQServer();
});
afterEach(() => {
server.reset();
});
it('should dispatch a command and return handler response', async () => {
server.addHandler('nestjs-testing.orders.create', (data: unknown) =>
handleCreateOrder(data as { product: string; quantity: number }),
);
const result = await server.dispatchCommand('nestjs-testing.orders.create', {
product: 'WIDGET-42',
quantity: 3,
});
expect(result.executed).toBe(true);
expect(result.response).toMatchObject({ product: 'WIDGET-42', status: 'created' });
});
it('should dispatch an event to a handler', async () => {
const received: unknown[] = [];
server.addHandler('nestjs-testing.orders.notify', (data: unknown) => {
received.push(data);
});
await server.dispatchEvent('nestjs-testing.orders.notify', {
orderId: 'ORD-001',
message: 'Order shipped',
});
expect(received).toHaveLength(1);
});
it('should auto-ack a queue message when the handler completes', async () => {
server.addHandler('nestjs-testing.orders.process', (data: unknown) => {
// process the message
});
const result = await server.dispatchQueueMessage('nestjs-testing.orders.process', {
id: '123',
});
expect(result.acked).toBe(true);
});
it('should report error when handler throws', async () => {
server.addHandler('nestjs-testing.orders.fail', () => {
throw new Error('Validation failed');
});
const result = await server.dispatchCommand('nestjs-testing.orders.fail', {});
expect(result.executed).toBe(false);
expect(result.error).toContain('Validation failed');
});
});You can also test decorated handlers — or @nestjs/cqrs handlers — through a full NestJS TestingModule with the real DI container and no mocks at all. Build the module with Test.createTestingModule({ providers: [MyHandler] }), resolve the handler, and call its method directly. The transport decorators only attach routing metadata, so the handler method itself is plain TypeScript you can invoke and assert on.
Integration Testing
When a live broker is available, an integration test exercises the full round-trip — serialization, gRPC transport, broker, and handler execution — instead of mocks. The transport's integration-test example builds a real module and sends real messages over the wire. Start a broker first; the gRPC API listens on 50000 and the shared HTTP and dashboard endpoints on 9090:
docker run -d \ --name kubemq \ -p 50000:50000 \ -p 9090:9090 \ -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY \ europe-docker.pkg.dev/kubemq/images/kubemq:nextThen register the real KubeMQModule (not forTest) and assert on responses that came back from the broker:
import 'reflect-metadata';
import { Test, TestingModule } from '@nestjs/testing';
import { KubeMQModule } from '@kubemq/nestjs-transport';
import { OrderService } from './order.service.js';
describe('OrderService (integration)', () => {
let module: TestingModule;
let orderService: OrderService;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
KubeMQModule.register({
name: 'KUBEMQ_SERVICE',
address: process.env.KUBEMQ_ADDRESS ?? 'localhost:50000',
clientId: 'integration-test',
}),
],
providers: [OrderService],
}).compile();
orderService = module.get(OrderService);
});
afterAll(async () => {
await module.close();
});
it('round-trips a command through a live broker', async () => {
const result = await orderService.createOrder('WIDGET-42', 5);
expect(result).toHaveProperty('orderId');
});
});Keep mock-based unit tests and broker-backed integration tests in separate suites. Unit tests run anywhere with no setup; integration tests require a reachable broker and are best gated behind an environment check or a dedicated CI job.
Troubleshooting
Related
- Module Configuration for
forRoot/registerand the fullforTestreference - Usage for the handler decorators and the
KubeMQRecordbuilder - API reference for the complete export surface, including the testing subpath
Was this page helpful?