AWS SDK v3 Client mock
Easy and powerful mocking of AWS SDK v3 Clients.

Features:
- 🌊 fluent interface - declaring behavior is short and readable
- 🔍 matching options - defining mock behavior by Command type and/or its input payload
- 🕵️ spying - checking if Commands were actually send
- 🖋️ fully typed - same type control for declaring mock's behavior as when writing regular code
- ✅ fully tested - reliable mocks help instead of impeding
In action:

Table of Contents
About AWS SDK v3
The AWS SDK for JavaScript version 3,
is the new version of SDK to use in Node.js and browser.
It comes with modular architecture and improved typing,
thanks to being written in TypeScript.
The recommended way of using it is to create a Client
and use it to send Commands
.
For example, using SNS Client to publish a message to a topic looks like that:
import {PublishCommand, SNSClient} from '@aws-sdk/client-sns';
const sns = new SNSClient({});
const result = await sns.send(new PublishCommand({
TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic',
Message: 'My message',
}));
console.log(`Message published, id: ${result.MessageId}`);
This library provides an easy way to mock sending Commands
and define returned results depending on the Command
type and payload.
Usage
Install
npm install -D aws-sdk-client-mock
or
yarn add -D aws-sdk-client-mock
Import
CommonJS:
const {mockClient} = require('aws-sdk-client-mock');
TypeScript / ES6:
import {mockClient} from 'aws-sdk-client-mock';
Mock
Create mock for all instances or for given instance of the AWS SDK Client:
const snsMock = mockClient(SNSClient);
const dynamoDB = new DynamoDBClient({});
const dynamoDBMock = mockClient(dynamoDB);
By default, mocked Client#send()
method returns undefined
.
Using the obtained mock instance, you can specify the mock behavior
on receiving various commands to send.
Specify default mock behavior:
snsMock.onAnyCommand().resolves({});
snsMock.resolves({});
Specify mock behavior on receiving given command only:
snsMock
.on(PublishCommand)
.resolves({
MessageId: '12345678-1111-2222-3333-111122223333',
});
Specify mock behavior on receiving given command with given payload only:
snsMock
.on(PublishCommand, {
TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic',
Message: 'My message',
})
.resolves({
MessageId: '12345678-4444-5555-6666-111122223333',
});
Not all payload parameters must be defined to match
(you can force strict matching by passing third param strict: true
):
snsMock
.on(PublishCommand, {
Message: 'My message',
})
.resolves({
MessageId: '12345678-4444-5555-6666-111122223333',
});
Specify mock behavior on receiving given payload only:
snsMock
.onAnyCommand({
TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic',
Message: 'My message',
})
.resolves({
MessageId: '12345678-4444-5555-6666-111122223333',
});
Multiple behaviors (for different commands and payloads) may be specified
for a single mock:
snsMock
.resolves({
MessageId: '12345678-1111-2222-3333-111122223333'
})
.on(PublishCommand)
.resolves({
MessageId: '12345678-4444-5555-6666-111122223333'
})
.on(PublishCommand, {
TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic',
Message: 'My message',
})
.resolves({
MessageId: '12345678-7777-8888-9999-111122223333',
});
Specify mock throwing an error:
snsMock
.rejects('mocked rejection');
Specify custom mock function:
snsMock
.callsFake(input => {
if (input.Message === 'My message') {
return {MessageId: '12345678-1111-2222-3333-111122223333'};
} else {
return {MessageId: '12345678-4444-5555-6666-111122223333'};
}
});
Inspect received calls:
snsMock.calls();
snsMock.call(0);
Under the hood, the library uses Sinon.js stub
.
You can get the stub instance to configure and use it directly:
const snsSendStub = snsMock.send;
DynamoDB DocumentClient
You can mock the DynamoDBDocumentClient
just like any other Client:
import {DynamoDBDocumentClient, QueryCommand} from '@aws-sdk/lib-dynamodb';
const ddbMock = mockClient(DynamoDBDocumentClient);
ddbMock.on(QueryCommand).resolves({
Items: [{pk: 'a', sk: 'b'}],
});
Paginated operations
To mock a paginated operation
results, simply mock the corresponding Command:
import {DynamoDBClient, paginateQuery, QueryCommand} from '@aws-sdk/client-dynamodb';
import {marshall} from '@aws-sdk/util-dynamodb';
const dynamodbMock = mockClient(DynamoDBClient);
dynamodbMock.on(QueryCommand).resolves({
Items: [
marshall({pk: 'a', sk: 'b'}),
marshall({pk: 'c', sk: 'd'}),
],
});
const dynamodb = new DynamoDBClient({});
const paginator = paginateQuery({client: dynamodb}, {TableName: 'mock'});
const items = [];
for await (const page of paginator) {
items.push(...page.Items || []);
}
API Reference
See the full API Reference.
AWS Lambda example
Example below uses Jest as a test framework, but mocks will work with any testing library.
Let's take a simple Lambda function that takes a list of messages,
sends them to SNS topic and returns message IDs:
import {PublishCommand, SNSClient} from '@aws-sdk/client-sns';
const snsTopicArn = process.env.SNS_TOPIC_ARN || '';
const sns = new SNSClient({});
export const handler = async (event: Event): Promise<string[]> => {
const promises = event.messages.map(async (msg, idx) => {
const publish = await sns.send(new PublishCommand({
TopicArn: snsTopicArn,
Message: msg,
}));
return publish.MessageId!;
});
return await Promise.all(promises);
};
interface Event {
messages: string[];
}
Then the tests could look like this:
import {mockClient} from '@m-radzikowski/aws-sdk-client-mock';
import {PublishCommand, SNSClient} from '@aws-sdk/client-sns';
import {handler} from '../src';
const snsMock = mockClient(SNSClient);
beforeEach(() => {
snsMock.reset();
});
it('message IDs are returned', async () => {
snsMock.on(PublishCommand).resolves({
MessageId: '12345678-1111-2222-3333-111122223333',
});
const result = await handler({
messages: ['one', 'two', 'three']
});
expect(result).toHaveLength(3);
expect(result[0]).toBe('12345678-1111-2222-3333-111122223333');
});
it('SNS Client is called', async () => {
snsMock.on(PublishCommand).resolves({
MessageId: '111-222-333',
});
await handler({
messages: ['qq', 'xx']
});
expect(snsMock.calls()).toHaveLength(2);
});
For more examples, see the unit tests.
Caveats
Order of mock behaviors
Wider Command matchers must be declared first,
otherwise, they will take precedence over previous ones.
In this case, all PublishCommand
sends will return message ID 222
:
snsMock
.on(PublishCommand, myInput).resolves({MessageId: '111'})
.on(PublishCommand).resolves({MessageId: '222'});
If the order of the declarations is switched, sends with input matching myInput
will return ID 111
and all others 222
.
It works similarly with onAnyCommand()
.
Order of type and instance mocks
When you create both a Client type mock
and a specific Client instance mock(s),
you need to declare type mock last.
Otherwise, the other instances will not be mocked.
Right now if you create a mock for the Client type,
and then mock a specific instance of this Client,
with the order of mocking as here:
const sns1 = new SNSClient({});
mockClient(SNSClient).resolves({MessageId: '123'});
const sns2 = new SNSClient({});
mockClient(sns2).resolves({MessageId: '456'});
const sns3 = new SNSClient({});
Declaring mocks in this order will fix it:
const sns1 = new SNSClient({});
const sns2 = new SNSClient({});
mockClient(sns2).resolves({MessageId: '456'});
mockClient(SNSClient).resolves({MessageId: '123'});
const sns3 = new SNSClient({});
PRs to fix this are welcome.