npm package: @message-in-the-middle/testing
Description: Testing utilities and mocks for message-middleware
Latest version: 0.1.3

# @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

```bash
# 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/testing
```

**Note:** This is a dev dependency. Don't use in production code.

## Quick Start

### Basic Test with Test Utils

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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:

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

1. **Use Test Harness for Middleware** - Test middleware in isolation before integration
2. **Use Mock Store for Persistence** - Avoid real database in unit tests
3. **Reset Mocks** - Always reset mocks between tests
4. **Test Error Cases** - Test both success and failure paths
5. **Keep Tests Fast** - Use mocks and in-memory stores for speed

## API Reference

### Test Utilities

```typescript
// 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

```typescript
class MiddlewareTestHarness<T = any> {
  constructor(middleware: InboundMiddleware<T>)
  execute(context: Partial<MessageContext<T>>): Promise<MessageContext<T>>
  getExecutionCount(): number
  reset(): void
}
```

### Mock Message Store

```typescript
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/)** - Core library
- **[@message-in-the-middle/persistence-core](../persistence-core/)** - Persistence interfaces
- **[@message-in-the-middle/store-memory](../store-memory/)** - In-memory store

## Documentation

- [Main README](../../README.md) - Complete documentation
- [Contributing](../../CONTRIBUTING.md) - How to contribute
- [Examples](../../examples/) - Usage examples

## License

MIT

## Links

- [GitHub Repository](https://github.com/gorannovosel/message-in-the-middle)
- [Report Issues](https://github.com/gorannovosel/message-in-the-middle/issues)