Unit Tests
This guide explains how to write unit tests for services and handlers in the MBC CQRS Serverless framework.
Mocking Dependencies
The framework uses @golevelup/ts-jest for creating mocks. The createMock() function automatically generates mock implementations for any interface or class.
Basic Usage of createMock()
import { createMock } from '@golevelup/ts-jest'
import { Test } from '@nestjs/testing'
import { ConfigService } from '@nestjs/config'
const moduleRef = await Test.createTestingModule({
providers: [MyService],
})
.useMocker(createMock) // Auto-mock all dependencies
.compile()
Custom Mock Implementation
import { createMock } from '@golevelup/ts-jest'
import { ConfigService } from '@nestjs/config'
const config = {
NODE_ENV: 'test',
APP_NAME: 'my-app',
}
const moduleRef = await Test.createTestingModule({
providers: [
MyService,
{
provide: ConfigService,
useValue: createMock<ConfigService>({
get: jest.fn((key) => config[key] ?? 'default'),
}),
},
],
}).compile()
Mocking AWS SDK Clients
Use aws-sdk-client-mock to mock AWS SDK v3 clients:
import { mockClient } from 'aws-sdk-client-mock'
import 'aws-sdk-client-mock-jest'
import { DynamoDBClient, PutItemCommand, GetItemCommand } from '@aws-sdk/client-dynamodb'
describe('MyService', () => {
const dynamoDBMock = mockClient(DynamoDBClient)
beforeEach(() => {
dynamoDBMock.reset()
})
afterEach(() => {
dynamoDBMock.reset()
})
it('should put item to DynamoDB', async () => {
// Arrange: Set up mock response
dynamoDBMock.on(PutItemCommand).resolves({})
// Action: Execute the method
await myService.saveItem({ pk: 'test', sk: 'item' })
// Assert: Verify the mock was called
expect(dynamoDBMock).toHaveReceivedCommandTimes(PutItemCommand, 1)
expect(dynamoDBMock).toHaveReceivedCommandWith(PutItemCommand, {
TableName: 'my-table',
Item: expect.objectContaining({
pk: { S: 'test' },
sk: { S: 'item' },
}),
})
})
})
Complete Test Example
Here is a complete example based on the framework's actual test patterns:
import { createMock } from '@golevelup/ts-jest'
import { Test } from '@nestjs/testing'
import { mockClient } from 'aws-sdk-client-mock'
import 'aws-sdk-client-mock-jest'
import { DynamoDBClient, GetItemCommand, PutItemCommand } from '@aws-sdk/client-dynamodb'
import { ConfigService } from '@nestjs/config'
const config = {
NODE_ENV: 'test',
APP_NAME: 'my-app',
}
describe('CommandService', () => {
let commandService: CommandService
const dynamoDBMock = mockClient(DynamoDBClient)
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
CommandService,
DynamoDbService,
{
provide: ConfigService,
useValue: createMock<ConfigService>({
get: jest.fn((key) => config[key]),
}),
},
],
})
.useMocker(createMock) // Auto-mock remaining dependencies
.compile()
commandService = moduleRef.get<CommandService>(CommandService)
})
afterEach(() => {
jest.clearAllMocks()
dynamoDBMock.reset()
})
describe('getLatestItem', () => {
it('should return the latest item', async () => {
// Arrange
const key = { pk: 'master', sk: 'test' }
// Action
const item = await commandService.getLatestItem(key)
// Assert
expect(item).toBeDefined()
expect(item?.pk).toBe('master')
})
it('should return null when item not found', async () => {
// Arrange
const key = { pk: 'master', sk: 'nonexistent' }
// Action
const item = await commandService.getLatestItem(key)
// Assert
expect(item).toBeNull()
})
})
})
Key Testing Patterns
Test Structure
Each test follows the Arrange-Act-Assert pattern:
- Arrange: Set up test data and mock responses
- Act: Execute the method being tested
- Assert: Verify the results and mock interactions
Using describe and it
Use describe to group related tests and it for individual test cases:
describe('ServiceName', () => {
describe('methodName', () => {
it('should do something when condition is met', async () => {
// test implementation
})
it('should throw error when input is invalid', async () => {
// test implementation
})
})
})
Testing Error Cases
import { BadRequestException } from '@nestjs/common';
it('should throw BadRequestException when item not found', async () => {
const invalidKey = { pk: 'invalid', sk: 'key', version: 1 }
await expect(
commandService.publishPartialUpdateAsync(invalidKey, { invokeContext: {} })
).rejects.toThrow(BadRequestException)
})
Advanced AWS SDK Mock Patterns
Mocking Multiple AWS Services
When testing services that interact with multiple AWS services, set up mocks for each client:
import { mockClient } from 'aws-sdk-client-mock'
import 'aws-sdk-client-mock-jest'
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns'
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs'
describe('MultiServiceTest', () => {
const dynamoMock = mockClient(DynamoDBClient)
const snsMock = mockClient(SNSClient)
const sqsMock = mockClient(SQSClient)
beforeEach(() => {
dynamoMock.reset()
snsMock.reset()
sqsMock.reset()
})
it('should publish to SNS after saving to DynamoDB', async () => {
dynamoMock.on(PutItemCommand).resolves({})
snsMock.on(PublishCommand).resolves({ MessageId: 'msg-123' })
await myService.saveAndNotify(data)
expect(dynamoMock).toHaveReceivedCommandTimes(PutItemCommand, 1)
expect(snsMock).toHaveReceivedCommandTimes(PublishCommand, 1)
})
})
Mocking Conditional Responses
Use callsFake to return different responses based on input:
import { marshall } from '@aws-sdk/util-dynamodb'
dynamoMock.on(GetItemCommand).callsFake((input) => {
if (input.Key.pk.S === 'existing-key') {
return {
Item: marshall({
pk: 'existing-key',
sk: 'item',
name: 'Test Item',
}),
}
}
return { Item: undefined } // Item not found
})
Mocking Errors
Test error handling by making mocks reject:
import { ConflictException } from '@nestjs/common'
it('should handle ConditionalCheckFailedException as 409 conflict', async () => {
const error = new Error('The conditional request failed')
error.name = 'ConditionalCheckFailedException'
dynamoMock.on(PutItemCommand).rejects(error)
await expect(myService.saveItem(data)).rejects.toThrow(ConflictException)
})
Using jest.mock() for Module-Level Mocking
For services that instantiate AWS clients internally, use jest.mock():
// Mock at the top of your test file
jest.mock('@aws-sdk/client-dynamodb', () => {
const original = jest.requireActual('@aws-sdk/client-dynamodb')
return {
...original,
DynamoDBClient: jest.fn().mockImplementation(() => ({
send: jest.fn(),
})),
}
})
Prefer aws-sdk-client-mock over jest.mock() when possible, as it provides better type safety and more detailed assertions like toHaveReceivedCommandWith.
Testing DataSyncHandlers
Test the up() and down() methods of an IDataSyncHandler by mocking database clients with createMock().
import { createMock } from '@golevelup/ts-jest'
import { Test } from '@nestjs/testing'
import { CommandModel } from '@mbc-cqrs-serverless/core'
import { OrderDataSyncHandler } from './order.handler'
import { PrismaService } from 'src/prisma.service'
describe('OrderDataSyncHandler', () => {
let handler: OrderDataSyncHandler
let prisma: jest.Mocked<PrismaService>
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [OrderDataSyncHandler],
})
.useMocker(createMock) // Auto-mock all dependencies
.compile()
handler = moduleRef.get(OrderDataSyncHandler)
prisma = moduleRef.get(PrismaService)
})
describe('up', () => {
it('should upsert record when command is processed', async () => {
// Arrange: build a representative CommandModel
const cmd = {
pk: 'ORDER#mbc',
sk: 'ORD-001',
tenantCode: 'mbc',
version: 1,
type: 'ORDER',
attributes: { customerId: 'C-001', total: 150 },
} as unknown as CommandModel
prisma.order.upsert.mockResolvedValue({ id: 'ORD-001' } as any)
await handler.up(cmd)
expect(prisma.order.upsert).toHaveBeenCalledWith(
expect.objectContaining({ where: { sk: 'ORD-001' } }),
)
})
})
describe('down', () => {
it('should delete record on rollback', async () => {
const cmd = {
pk: 'ORDER#mbc',
sk: 'ORD-001',
tenantCode: 'mbc',
} as unknown as CommandModel
prisma.order.delete.mockResolvedValue({ id: 'ORD-001' } as any)
await handler.down(cmd)
expect(prisma.order.delete).toHaveBeenCalledWith({
where: { sk: 'ORD-001' },
})
})
})
})
If your handler does not use Prisma (e.g., writes directly to DynamoDB), inject and mock DynamoDbService or DataService from @mbc-cqrs-serverless/core instead.
Related Documentation
- E2E Testing - End-to-end testing guide
- Testing - Testing overview
- Command Service - CommandService API (the subject of most unit tests)
- Data Service - DataService API (query operations to mock in tests)
- Debugging Guide - Debug test failures