aws-sdk-client-mock
aws-sdk-client-mock is a unit-testing helper for the modular AWS SDK for JavaScript v3. It replaces an AWS client's send method with a Sinon stub, then lets a test return or reject typed responses for particular Command classes and partial inputs. It can mock every instance of a client class or one client object, record sent commands, and provide sequential or computed responses. It never contacts AWS and does not simulate a service, credentials, request signing, retries, middleware, or network behavior.
A focused and readable unit-test tool for ordinary AWS SDK v3 Command flows. Keep a smaller set of emulator or real-service tests for everything the send stub deliberately skips, and watch compatibility before upgrading AWS SDK or your test runner.
Use it if
- Your Node.js or TypeScript unit tests call AWS SDK v3 clients through client.send(new Command(input)) and should run without credentials or network access
- You want command-aware response types and partial input matching instead of hand-stubbing send in every test
- You need to inspect which AWS Commands were sent, their inputs, and their order while keeping the production client API intact
- You use Jest, Vitest, Mocha, or another test runner and are comfortable managing Sinon-backed mock lifecycle explicitly
- You need confidence in IAM, request signing, serialization, retries, endpoint resolution, middleware, or AWS service semantics: the library replaces only Client#send and never runs those real paths
- You use AWS SDK v2: this package is built around v3 Client and Command classes; aws-sdk-mock is the closer fit for v2-style service objects
- Your dependency tree contains several @smithy/types versions and you cannot align or override them; the README documents otherwise valid clients failing TypeScript assignment because their middleware-stack types differ
- You rely heavily on helpers outside ordinary send calls, such as presigning or waiters: the issue tracker has long-running requests for those cases, so an emulator or direct helper mock is a better fit
- You need a low-maintenance dependency: 4.1.0 bundles Sinon and its types, the latest release and main-branch code change were in October 2024, and open issues report compatibility trouble with newer AWS SDK and test-runner versions
Setup reality
Install aws-sdk-client-mock as a development dependency beside the AWS SDK v3 client packages your code already uses. It includes Sinon, @types/sinon, and tslib as runtime dependencies; you do not need to install Sinon unless you want to share a custom sandbox. No AWS account, region, credentials, emulator, or config file is needed because the helper replaces Client#send. That convenience creates the first surprise: an unconfigured mock resolves to undefined, so a forgotten behavior may fail later in application code rather than at the send call. Reset each mock before or after every test or behavior and call history will leak between cases. restore is different from reset: restore puts the real send method back, which can accidentally allow network-bound code to run. TypeScript failures often come from duplicated @smithy/types versions across AWS packages; the documented remedies are aligning every @aws-sdk package, reinstalling, or forcing one Smithy type version. Explicit AwsClientStub annotations also require strictFunctionTypes or strict mode. Matching is partial by default, with strict matching enabled by the third argument. Declaration order matters because broader matchers registered later can eclipse earlier specific ones. Class and instance mocks have a separate ordering trap: create instance mocks first and the class mock last. Jest and Vitest matchers live in the separate aws-sdk-client-mock-jest package, and Vitest must import its /vitest entry. Under Mocha, create the mock inside beforeEach rather than global scope to avoid test files replacing one another's stubs. S3 GetObject bodies require @smithy/util-stream and sdkStreamMixin, while @aws-sdk/lib-storage Upload tests must mock each command that Upload sends internally. These are unit stubs, not a substitute for a small integration-test layer against AWS or LocalStack.
Patterns
Return a typed response for one Commandmock-command-response
import { mockClient } from 'aws-sdk-client-mock';
import { PublishCommand, SNSClient } from '@aws-sdk/client-sns';
const snsMock = mockClient(SNSClient);
snsMock.on(PublishCommand).resolves({ MessageId: 'msg-123' });
const sns = new SNSClient({});
const result = await sns.send(new PublishCommand({
TopicArn: 'arn:aws:sns:us-east-1:111111111111:events',
Message: 'hello',
}));Without a matching behavior, the mocked send method resolves to undefined. Configure every path your code reads from.
Match partial or exact Command inputmatch-command-input
snsMock
.on(PublishCommand, { Message: 'hello' })
.resolves({ MessageId: 'partial' });
snsMock
.on(PublishCommand, {
TopicArn: 'arn:aws:sns:us-east-1:111111111111:events',
Message: 'strict',
}, true)
.resolves({ MessageId: 'exact' });Input matching is partial by default. Pass true as the third argument only when extra input fields should make the matcher fail.
Declare broad behavior before specific behaviororder-mock-behaviors
snsMock
.resolves({ MessageId: 'any-command' })
.on(PublishCommand)
.resolves({ MessageId: 'any-publish' })
.on(PublishCommand, { Message: 'priority' })
.resolves({ MessageId: 'priority-publish' });Register wider matchers first. A broad matcher declared later takes precedence and can make an earlier specific response unreachable.
Model consecutive responsesreturn-sequential-results
snsMock
.on(PublishCommand)
.resolvesOnce({ MessageId: 'first' })
.resolvesOnce({ MessageId: 'second' })
.resolves({ MessageId: 'later' });After the once-only responses are consumed, the final resolves value becomes the fallback for subsequent matching calls.
Simulate an AWS-style failurereject-with-aws-error
const error = new Error('rate exceeded');
error.name = 'ThrottlingException';
Object.assign(error, { $metadata: { httpStatusCode: 429 } });
snsMock
.on(PublishCommand)
.rejects(error);rejects accepts a string, Error, or error-like object and normalizes it to Error. Set the name and metadata your application actually branches on.
Build a response from input and client configurationcompute-response-from-client
snsMock.on(PublishCommand).callsFake(async (input, getClient) => {
const client = getClient();
const region = await client.config.region();
return { MessageId: `${region}:${input.Message}` };
});getClient returns the client associated with the current call. The fake does not run AWS middleware or validate the input.
Mock only one client instancemock-one-client-instance
const primary = new SNSClient({ region: 'us-east-1' });
const secondary = new SNSClient({ region: 'eu-west-1' });
const primaryMock = mockClient(primary);
primaryMock.resolves({ MessageId: 'primary-only' });
await primary.send(new PublishCommand({ Message: 'test' }));
// secondary.send remains real unless a class mock also existsIf mixing instance and class mocks, create instance mocks first and declare the class mock last. The reverse order leaves surprising instances unmocked.
Keep tests isolated and restore real behaviorreset-and-restore
const snsMock = mockClient(SNSClient);
beforeEach(() => {
snsMock.reset();
});
afterAll(() => {
snsMock.restore();
});reset clears behavior and history but keeps send mocked. resetHistory preserves behavior. restore removes the stub and puts the original send method back.
Inspect matching Command callsinspect-command-calls
const publishes = snsMock.commandCalls(
PublishCommand,
{ Message: 'hello' }
);
expect(publishes).toHaveLength(2);
expect(publishes[0].args[0].input.TopicArn).toContain(':events');
const secondPublish = snsMock.commandCall(1, PublishCommand);commandCall uses a zero-based index among calls of that Command, while call uses a zero-based index among every recorded send call.
Add Jest or Vitest Command matchersuse-test-runner-matchers
// Jest: import 'aws-sdk-client-mock-jest';
// Vitest uses the entry below:
import 'aws-sdk-client-mock-jest/vitest';
expect(snsMock).toHaveReceivedCommandWith(
PublishCommand,
{ Message: expect.stringContaining('hello') }
);Install aws-sdk-client-mock-jest separately. Import its /vitest entry under Vitest; the default entry targets Jest.
Mock DynamoDBDocumentClient commandsmock-dynamodb-document-client
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, QueryCommand } from '@aws-sdk/lib-dynamodb';
import { mockClient } from 'aws-sdk-client-mock';
const ddbMock = mockClient(DynamoDBDocumentClient);
ddbMock.on(QueryCommand).resolves({
Items: [{ pk: 'USER#1', sk: 'PROFILE' }],
});
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
await ddb.send(new QueryCommand({ TableName: 'app' }));Mock DynamoDBDocumentClient, not the lower-level DynamoDBClient, when production code sends lib-dynamodb Command classes.
Return an SDK-compatible S3 streammock-s3-stream-body
import { Readable } from 'node:stream';
import { sdkStreamMixin } from '@smithy/util-stream';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
const s3Mock = mockClient(S3Client);
const body = sdkStreamMixin(Readable.from(['hello']));
s3Mock.on(GetObjectCommand).resolves({ Body: body });
const s3 = new S3Client({});
const result = await s3.send(new GetObjectCommand({ Bucket: 'b', Key: 'k' }));
expect(await result.Body?.transformToString()).toBe('hello');Install @smithy/util-stream. A plain Node stream lacks the transformToString and related mixin methods attached by the real SDK.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| sinon | npm | Stub a known client instance's send method directly when command matching and AWS-specific types add more machinery than value |
| aws-sdk-mock | npm | Mock service methods in a codebase that still uses the monolithic AWS SDK for JavaScript v2 |
| testcontainers | npm | Run a LocalStack container when tests must exercise SDK serialization, endpoints, retries, and service-like behavior |