@message-in-the-middle/testing
v0.1.3
Published
Testing utilities and mocks for message-middleware
Maintainers
Readme
@message-in-the-middle/testing
⚠️ Work in Progress Is this library production-ready? No. Is this library safe? No. When will it be ready? Soon™ (maybe tomorrow, maybe never). Why is it public? Experiment
message-in-the-middle is to Express.js what your message queue processing is to HTTP request processing. Just as Express provides a middleware pattern for HTTP requests, this library provides a middleware pattern for processing queue messages.
Why This Exists
Processing queue messages usually means copy-pasting the same boilerplate: parse JSON, validate, log, retry, deduplicate, route to handlers. This library lets you compose that logic as middlewares.
Testing utilities and mocks for message-middleware. Simplify unit and integration testing of message processing pipelines.
This package provides test helpers, mocks, and utilities to make testing middleware and message processing logic straightforward and reliable.
Features
- 🧪 Test Harness - Test middleware in isolation
- 🎭 Mock Store - In-memory message store with spy capabilities
- 🛠️ Test Utilities - Helper functions for creating test contexts
- 🎯 Type-Safe - Full TypeScript support
- 🚀 Framework Agnostic - Works with any test framework (Vitest, Jest, Mocha)
Installation
# npm
npm install --save-dev @message-in-the-middle/testing
# pnpm
pnpm add -D @message-in-the-middle/testing
# yarn
yarn add --dev @message-in-the-middle/testingNote: This is a dev dependency. Don't use in production code.
Quick Start
Basic Test with Test Utils
import { describe, it, expect } from 'vitest';
import { createMockContext } from '@message-in-the-middle/testing';
import { ParseJsonInboundMiddleware } from '@message-in-the-middle/core';
describe('ParseJsonInboundMiddleware', () => {
it('should parse JSON string', async () => {
const middleware = new ParseJsonInboundMiddleware();
const context = createMockContext({
message: '{"order": 123}'
});
await middleware.process(context, async () => {});
expect(context.message).toEqual({ order: 123 });
});
});Test Utilities
createMockContext()
Create a mock message context for testing:
import { createMockContext } from '@message-in-the-middle/testing';
// Minimal context
const context = createMockContext();
// With message
const context = createMockContext({
message: { order: 123 }
});
// With all fields
const context = createMockContext({
message: { order: 123 },
raw: { MessageId: 'abc-123' },
metadata: { source: 'sqs' },
attributes: { timestamp: Date.now() }
});createMockMessage()
Create a mock message for testing:
import { createMockMessage } from '@message-in-the-middle/testing';
// Simple message
const message = createMockMessage({
action: 'CREATE_ORDER',
orderId: '123'
});
// With metadata
const message = createMockMessage(
{ action: 'CREATE_ORDER' },
{ source: 'api' }
);Middleware Test Harness
Test middleware in isolation with a simple harness:
import { describe, it, expect } from 'vitest';
import { MiddlewareTestHarness } from '@message-in-the-middle/testing';
import { ValidateInboundMiddleware } from '@message-in-the-middle/core';
describe('ValidateInboundMiddleware', () => {
it('should validate message', async () => {
const validator = async (msg) => {
if (!msg.orderId) throw new Error('Missing orderId');
return msg;
};
const middleware = new ValidateInboundMiddleware(validator);
const harness = new MiddlewareTestHarness(middleware);
// Valid message - should not throw
await harness.execute({
message: { orderId: '123', amount: 100 }
});
// Invalid message - should throw
await expect(harness.execute({
message: { amount: 100 }
})).rejects.toThrow('Missing orderId');
});
});Test Harness API
class MiddlewareTestHarness<T = any> {
constructor(middleware: InboundMiddleware<T>);
// Execute middleware with context
async execute(context: Partial<MessageContext<T>>): Promise<MessageContext<T>>;
// Get execution count
getExecutionCount(): number;
// Reset harness
reset(): void;
}Mock Message Store
Mock implementation of MessageStore with spy capabilities:
import { describe, it, expect } from 'vitest';
import { MockMessageStore } from '@message-in-the-middle/testing';
import { PersistenceInboundMiddleware, MessageStatus } from '@message-in-the-middle/persistence-core';
describe('PersistenceInboundMiddleware', () => {
it('should store failed messages', async () => {
const store = new MockMessageStore();
const middleware = new PersistenceInboundMiddleware(store, {
storeOn: ['error']
});
const context = createMockContext({
message: { order: 123 }
});
// Simulate error
try {
await middleware.process(context, async () => {
throw new Error('Processing failed');
});
} catch (error) {
// Expected
}
// Verify message was stored
expect(store.getSaveCount()).toBe(1);
const saved = store.getAllMessages();
expect(saved).toHaveLength(1);
expect(saved[0].status).toBe(MessageStatus.FAILED);
});
});Mock Store API
class MockMessageStore implements MessageStore {
// Standard MessageStore methods
save(message: StoredMessage): Promise<void>;
findById(id: string): Promise<StoredMessage | null>;
findByStatus(status: MessageStatus, options?: QueryOptions): Promise<StoredMessage[]>;
// ... other MessageStore methods
// Spy methods
getSaveCount(): number;
getUpdateCount(): number;
getDeleteCount(): number;
getAllMessages(): StoredMessage[];
getMessageById(id: string): StoredMessage | undefined;
reset(): void;
}Testing Patterns
Pattern 1: Test Middleware in Isolation
import { describe, it, expect } from 'vitest';
import { MiddlewareTestHarness, createMockContext } from '@message-in-the-middle/testing';
import { LogInboundMiddleware } from '@message-in-the-middle/core';
describe('LogInboundMiddleware', () => {
it('should log message processing', async () => {
const logs: string[] = [];
const logger = {
log: (msg: string) => logs.push(msg)
};
const middleware = new LogInboundMiddleware(logger);
const harness = new MiddlewareTestHarness(middleware);
await harness.execute({
message: { order: 123 }
});
expect(logs.length).toBeGreaterThan(0);
expect(logs[0]).toContain('Processing message');
});
});Pattern 2: Test Custom Middleware
import { describe, it, expect, beforeEach } from 'vitest';
import { createMockContext } from '@message-in-the-middle/testing';
import { InboundMiddleware, MessageContext } from '@message-in-the-middle/core';
class CustomEnrichmentMiddleware implements InboundMiddleware {
async process(ctx: MessageContext, next: () => Promise<void>): Promise<void> {
ctx.metadata.enriched = true;
ctx.metadata.timestamp = Date.now();
await next();
}
}
describe('CustomEnrichmentMiddleware', () => {
let middleware: CustomEnrichmentMiddleware;
beforeEach(() => {
middleware = new CustomEnrichmentMiddleware();
});
it('should enrich message with metadata', async () => {
const context = createMockContext({
message: { order: 123 }
});
let nextCalled = false;
await middleware.process(context, async () => {
nextCalled = true;
});
expect(nextCalled).toBe(true);
expect(context.metadata.enriched).toBe(true);
expect(context.metadata.timestamp).toBeDefined();
});
});Pattern 3: Test Pipeline Integration
import { describe, it, expect } from 'vitest';
import { MockMessageStore, createMockContext } from '@message-in-the-middle/testing';
import {
MessageMiddlewareManager,
ParseJsonInboundMiddleware,
ValidateInboundMiddleware
} from '@message-in-the-middle/core';
import { PersistenceInboundMiddleware, MessageStatus } from '@message-in-the-middle/persistence-core';
describe('Message Processing Pipeline', () => {
it('should process valid message through pipeline', async () => {
const store = new MockMessageStore();
const manager = new MessageMiddlewareManager();
manager
.addInboundMiddleware(new ParseJsonInboundMiddleware())
.addInboundMiddleware(new ValidateInboundMiddleware(async (msg) => {
if (!msg.orderId) throw new Error('Missing orderId');
return msg;
}))
.addInboundMiddleware(new PersistenceInboundMiddleware(store, {
storeOn: ['always']
}));
// Process valid message
const result = await manager.processInbound(
JSON.stringify({ orderId: '123', amount: 100 })
);
expect(result.message).toEqual({ orderId: '123', amount: 100 });
expect(store.getSaveCount()).toBe(1);
const saved = store.getAllMessages();
expect(saved[0].status).toBe(MessageStatus.SUCCEEDED);
});
it('should handle validation errors', async () => {
const store = new MockMessageStore();
const manager = new MessageMiddlewareManager();
manager
.addInboundMiddleware(new ParseJsonInboundMiddleware())
.addInboundMiddleware(new ValidateInboundMiddleware(async (msg) => {
if (!msg.orderId) throw new Error('Missing orderId');
return msg;
}))
.addInboundMiddleware(new PersistenceInboundMiddleware(store, {
storeOn: ['error']
}));
// Process invalid message
await expect(
manager.processInbound(JSON.stringify({ amount: 100 }))
).rejects.toThrow('Missing orderId');
expect(store.getSaveCount()).toBe(1);
const saved = store.getAllMessages();
expect(saved[0].status).toBe(MessageStatus.FAILED);
expect(saved[0].errorMessage).toContain('Missing orderId');
});
});Pattern 4: Test Message Handlers
import { describe, it, expect } from 'vitest';
import { createMockContext } from '@message-in-the-middle/testing';
import { MessageContext } from '@message-in-the-middle/core';
async function handleCreateOrder(ctx: MessageContext): Promise<void> {
const { orderId, amount } = ctx.message;
// Business logic
if (amount <= 0) {
throw new Error('Invalid amount');
}
ctx.metadata.orderCreated = true;
ctx.metadata.orderId = orderId;
}
describe('handleCreateOrder', () => {
it('should handle valid order', async () => {
const context = createMockContext({
message: { orderId: '123', amount: 100 }
});
await handleCreateOrder(context);
expect(context.metadata.orderCreated).toBe(true);
expect(context.metadata.orderId).toBe('123');
});
it('should reject invalid amount', async () => {
const context = createMockContext({
message: { orderId: '123', amount: -50 }
});
await expect(handleCreateOrder(context)).rejects.toThrow('Invalid amount');
});
});Pattern 5: Test with Message Dispatcher
import { describe, it, expect } from 'vitest';
import { createMockContext } from '@message-in-the-middle/testing';
import { MessageDispatcher, MessageContext } from '@message-in-the-middle/core';
describe('MessageDispatcher', () => {
it('should route messages to correct handler', async () => {
const dispatcher = new MessageDispatcher({ identifierField: 'action' });
const handlerCalls: string[] = [];
dispatcher.register('CREATE_ORDER', async (ctx: MessageContext) => {
handlerCalls.push('create');
});
dispatcher.register('UPDATE_ORDER', async (ctx: MessageContext) => {
handlerCalls.push('update');
});
// Test CREATE_ORDER
const createContext = createMockContext({
message: { action: 'CREATE_ORDER', orderId: '123' }
});
await dispatcher.dispatch(createContext);
expect(handlerCalls).toEqual(['create']);
// Test UPDATE_ORDER
const updateContext = createMockContext({
message: { action: 'UPDATE_ORDER', orderId: '123' }
});
await dispatcher.dispatch(updateContext);
expect(handlerCalls).toEqual(['create', 'update']);
});
});Test Framework Examples
Vitest
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createMockContext, MockMessageStore } from '@message-in-the-middle/testing';
describe('My Feature', () => {
let store: MockMessageStore;
beforeEach(() => {
store = new MockMessageStore();
});
afterEach(() => {
store.reset();
});
it('should work', async () => {
// Test code
});
});Jest
import { createMockContext, MockMessageStore } from '@message-in-the-middle/testing';
describe('My Feature', () => {
let store: MockMessageStore;
beforeEach(() => {
store = new MockMessageStore();
});
afterEach(() => {
store.reset();
});
test('should work', async () => {
// Test code
});
});Mocha + Chai
import { expect } from 'chai';
import { createMockContext, MockMessageStore } from '@message-in-the-middle/testing';
describe('My Feature', () => {
let store: MockMessageStore;
beforeEach(() => {
store = new MockMessageStore();
});
afterEach(() => {
store.reset();
});
it('should work', async () => {
// Test code
});
});Best Practices
- Use Test Harness for Middleware - Test middleware in isolation before integration
- Use Mock Store for Persistence - Avoid real database in unit tests
- Reset Mocks - Always reset mocks between tests
- Test Error Cases - Test both success and failure paths
- Keep Tests Fast - Use mocks and in-memory stores for speed
API Reference
Test Utilities
// Create mock context
function createMockContext<T = any>(
partial?: Partial<MessageContext<T>>
): MessageContext<T>
// Create mock message
function createMockMessage<T = any>(
message: T,
metadata?: Record<string, any>
): MessageContext<T>Middleware Test Harness
class MiddlewareTestHarness<T = any> {
constructor(middleware: InboundMiddleware<T>)
execute(context: Partial<MessageContext<T>>): Promise<MessageContext<T>>
getExecutionCount(): number
reset(): void
}Mock Message Store
class MockMessageStore implements MessageStore {
// MessageStore interface methods
save(message: StoredMessage): Promise<void>
findById(id: string): Promise<StoredMessage | null>
findByStatus(status: MessageStatus, options?: QueryOptions): Promise<StoredMessage[]>
findByError(errorType: string, options?: QueryOptions): Promise<StoredMessage[]>
updateStatus(id: string, updates: Partial<StoredMessage>): Promise<void>
count(filters?: QueryFilters): Promise<number>
delete(id: string): Promise<void>
destroy(): Promise<void>
// Spy methods
getSaveCount(): number
getUpdateCount(): number
getDeleteCount(): number
getAllMessages(): StoredMessage[]
getMessageById(id: string): StoredMessage | undefined
reset(): void
}Related Packages
- @message-in-the-middle/core - Core library
- @message-in-the-middle/persistence-core - Persistence interfaces
- @message-in-the-middle/store-memory - In-memory store
Documentation
- Main README - Complete documentation
- Contributing - How to contribute
- Examples - Usage examples
License
MIT
