Skip to main content

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:

  1. Arrange: Set up test data and mock responses
  2. Act: Execute the method being tested
  3. 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(),
})),
}
})
Best Practice

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.