The Principal Dev โ€“ Masterclass for Tech Leads

The Principal Dev โ€“ Masterclass for Tech Leads28-29 May

Join

aws-sdk-vitest-mock logo

AWS SDK Vitest Mock

A powerful, type-safe mocking library for AWS SDK v3 with Vitest

npm version Codacy Badge NPM Downloads GitHub Issues or Pull Requests CI Status
ESM Support Zero Dependencies ESLint Prettier Maintained: Yes


โœจ Features

๐Ÿ“ฆ Installation

bun add -D aws-sdk-vitest-mock

Or with other package managers:

npm install --save-dev aws-sdk-vitest-mock
yarn add -D aws-sdk-vitest-mock
pnpm add -D aws-sdk-vitest-mock

โœ… Compatibility

๐Ÿš€ Quick Start

Basic Usage

Note: mockClient() mocks all instances of a client class. Use mockClientInstance() when you need to mock a specific instance.

import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { mockClient } from "aws-sdk-vitest-mock";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

// Your application code
class DocumentService {
  constructor(private s3Client: S3Client) {}

  async getDocument(bucket: string, key: string) {
    const result = await this.s3Client.send(
      new GetObjectCommand({ Bucket: bucket, Key: key }),
    );
    return result.Body;
  }
}

describe("DocumentService", () => {
  let s3Mock: ReturnType<typeof mockClient>;
  let documentService: DocumentService;

  beforeEach(() => {
    // Mock all instances of S3Client
    s3Mock = mockClient(S3Client);

    // Any S3Client instance created after this will be mocked
    const s3Client = new S3Client({ region: "us-east-1" });
    documentService = new DocumentService(s3Client);
  });

  afterEach(() => {
    s3Mock.restore();
  });

  test("should retrieve document from S3", async () => {
    // Configure mock response
    s3Mock.on(GetObjectCommand).resolves({
      Body: "document content",
      ContentType: "text/plain",
    });

    // Test your application code
    const result = await documentService.getDocument("my-bucket", "doc.txt");

    expect(result).toBe("document content");
    expect(s3Mock).toHaveReceivedCommand(GetObjectCommand);
  });
});

๐ŸŽฏ Key Concepts

Understanding these concepts will help you use the library effectively:

๐Ÿ“– Usage Guide

Request Matching

// Partial matching (default)
s3Mock.on(GetObjectCommand, { Bucket: "bucket1" }).resolves({ Body: "data1" });

s3Mock.on(GetObjectCommand, { Bucket: "bucket2" }).resolves({ Body: "data2" });

// Strict matching
s3Mock
  .on(GetObjectCommand, { Bucket: "b", Key: "k" }, { strict: true })
  .resolves({ Body: "exact match" });

Sequential Responses

s3Mock
  .on(GetObjectCommand)
  .resolvesOnce({ Body: "first call" })
  .resolvesOnce({ Body: "second call" })
  .resolves({ Body: "subsequent calls" });

// First call returns 'first call'
// Second call returns 'second call'
// All other calls return 'subsequent calls'

Error Handling

s3Mock.on(GetObjectCommand).rejects(new Error("Not found"));

// Or with rejectsOnce
s3Mock
  .on(GetObjectCommand)
  .rejectsOnce(new Error("Temporary failure"))
  .resolves({ Body: "success" });

Custom Handlers

s3Mock.on(GetObjectCommand).callsFake(async (input, getClient) => {
  const client = getClient();
  console.log("Bucket:", input.Bucket);
  return { Body: `Dynamic response for ${input.Key}` };
});

Mocking Existing Instances

Use mockClientInstance() when you need to mock a client that's already been created:

// Your application service that uses an injected S3 client
class FileUploadService {
  constructor(private s3Client: S3Client) {}

  async uploadFile(bucket: string, key: string, data: string) {
    return await this.s3Client.send(
      new PutObjectCommand({ Bucket: bucket, Key: key, Body: data }),
    );
  }
}

test("should mock existing S3 client instance", async () => {
  // Client is already created (e.g., in application bootstrap)
  const s3Client = new S3Client({ region: "us-east-1" });
  const service = new FileUploadService(s3Client);

  // Mock the specific client instance
  const mock = mockClientInstance(s3Client);
  mock.on(PutObjectCommand).resolves({ ETag: "mock-etag" });

  // Test your service
  const result = await service.uploadFile("bucket", "key", "data");

  expect(result.ETag).toBe("mock-etag");
  expect(mock).toHaveReceivedCommand(PutObjectCommand);
});

Test Lifecycle Management

Use reset() to clear call history between assertions while keeping mock configurations. Use restore() to completely clean up mocking:

test("should handle multiple operations with same mock", async () => {
  const s3Mock = mockClient(S3Client);
  const client = new S3Client({});

  // Configure mock once
  s3Mock.on(GetObjectCommand).resolves({ Body: "file-content" });

  // First operation
  await client.send(
    new GetObjectCommand({ Bucket: "bucket", Key: "file1.txt" }),
  );
  expect(s3Mock).toHaveReceivedCommandTimes(GetObjectCommand, 1);

  // Reset clears call history but keeps mock configuration
  s3Mock.reset();
  expect(s3Mock).toHaveReceivedCommandTimes(GetObjectCommand, 0);

  // Second operation - mock still works
  await client.send(
    new GetObjectCommand({ Bucket: "bucket", Key: "file2.txt" }),
  );
  expect(s3Mock).toHaveReceivedCommandTimes(GetObjectCommand, 1);

  // Clean up completely
  s3Mock.restore();
});

๐Ÿ”ง AWS Service Examples

DynamoDB with Marshal/Unmarshal

Mock DynamoDB operations using marshal/unmarshal utilities for type-safe data handling:

import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { mockClient } from "aws-sdk-vitest-mock";
import {
  DynamoDBClient,
  GetItemCommand,
  PutItemCommand,
} from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";

// Your application service
class UserService {
  constructor(private dynamoClient: DynamoDBClient) {}

  async getUser(userId: string) {
    const result = await this.dynamoClient.send(
      new GetItemCommand({
        TableName: "Users",
        Key: marshall({ id: userId }),
      }),
    );

    return result.Item ? unmarshall(result.Item) : null;
  }

  async createUser(user: { id: string; name: string; email: string }) {
    await this.dynamoClient.send(
      new PutItemCommand({
        TableName: "Users",
        Item: marshall(user),
      }),
    );
  }
}

describe("UserService with DynamoDB", () => {
  let dynamoMock: ReturnType<typeof mockClient>;
  let userService: UserService;

  beforeEach(() => {
    dynamoMock = mockClient(DynamoDBClient);
    const dynamoClient = new DynamoDBClient({ region: "us-east-1" });
    userService = new UserService(dynamoClient);
  });

  afterEach(() => {
    dynamoMock.restore();
  });

  test("should get user by id", async () => {
    const mockUser = { id: "123", name: "John Doe", email: "john@example.com" };

    // Mock DynamoDB response with marshalled data
    dynamoMock.on(GetItemCommand).resolves({
      Item: marshall(mockUser),
    });

    const result = await userService.getUser("123");

    expect(result).toEqual(mockUser);
    expect(dynamoMock).toHaveReceivedCommandWith(GetItemCommand, {
      TableName: "Users",
      Key: marshall({ id: "123" }),
    });
  });

  test("should create new user", async () => {
    const newUser = {
      id: "456",
      name: "Jane Smith",
      email: "jane@example.com",
    };

    dynamoMock.on(PutItemCommand).resolves({});

    await userService.createUser(newUser);

    expect(dynamoMock).toHaveReceivedCommandWith(PutItemCommand, {
      TableName: "Users",
      Item: marshall(newUser),
    });
  });

  test("should return null for non-existent user", async () => {
    dynamoMock.on(GetItemCommand).resolves({}); // No Item in response

    const result = await userService.getUser("999");

    expect(result).toBeNull();
  });
});

๐Ÿš€ Advanced Features

Stream Mocking (S3)

Mock S3 operations that return streams with automatic environment detection:

// Mock with string content
s3Mock.on(GetObjectCommand).resolvesStream("Hello, World!");

// Mock with Buffer
s3Mock.on(GetObjectCommand).resolvesStream(Buffer.from("Binary data"));

// One-time stream response
s3Mock
  .on(GetObjectCommand)
  .resolvesStreamOnce("First call")
  .resolvesStream("Subsequent calls");

Paginator Support

Mock AWS SDK v3 pagination with automatic token handling. Tokens are the actual last item from each page (works for both DynamoDB and S3).

DynamoDB Pagination

import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
import { DynamoDBClient, ScanCommand } from '@aws-sdk/client-dynamodb';

// Create marshalled items (as they would be stored in DynamoDB)
const users = [
  { id: "user-1", name: "Alice", email: "alice@example.com" },
  { id: "user-2", name: "Bob", email: "bob@example.com" },
  { id: "user-3", name: "Charlie", email: "charlie@example.com" },
];

const marshalledUsers = users.map(user => marshall(user));

// Configure pagination
dynamoMock.on(ScanCommand).resolvesPaginated(marshalledUsers, {
  pageSize: 1,
  itemsKey: "Items",
  tokenKey: "LastEvaluatedKey",      // DynamoDB response key
  inputTokenKey: "ExclusiveStartKey"  // DynamoDB request key
});

// Page 1: Get first user
const page1 = await client.send(new ScanCommand({ TableName: "Users" }));
expect(page1.Items).toHaveLength(1);
// LastEvaluatedKey is the marshalled last item (object, not string!)
expect(page1.LastEvaluatedKey).toEqual(marshall({ id: "user-1", name: "Alice", ... }));

// Unmarshall the items
const page1Users = page1.Items.map(item => unmarshall(item));
console.log(page1Users[0]); // { id: "user-1", name: "Alice", ... }

// Page 2: Use LastEvaluatedKey to get next page
const page2 = await client.send(
  new ScanCommand({
    TableName: "Users",
    ExclusiveStartKey: page1.LastEvaluatedKey, // Pass the object directly
  })
);

// Page 3: Continue until LastEvaluatedKey is undefined
const page3 = await client.send(
  new ScanCommand({
    TableName: "Users",
    ExclusiveStartKey: page2.LastEvaluatedKey,
  })
);
expect(page3.LastEvaluatedKey).toBeUndefined(); // No more pages

S3 Pagination

import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';

const objects = Array.from({ length: 100 }, (_, i) => ({
  Key: `file-${i + 1}.txt`,
  Size: 1024,
  LastModified: new Date(),
}));

s3Mock.on(ListObjectsV2Command).resolvesPaginated(objects, {
  pageSize: 50,
  itemsKey: "Contents",
  tokenKey: "NextContinuationToken",
  inputTokenKey: "ContinuationToken"
});

// First page
const page1 = await client.send(
  new ListObjectsV2Command({ Bucket: "my-bucket" })
);
expect(page1.Contents).toHaveLength(50);
// NextContinuationToken is the last object from page 1
expect(page1.NextContinuationToken).toEqual({ Key: "file-50.txt", ... });

// Second page
const page2 = await client.send(
  new ListObjectsV2Command({
    Bucket: "my-bucket",
    ContinuationToken: page1.NextContinuationToken,
  })
);
expect(page2.Contents).toHaveLength(50);
expect(page2.NextContinuationToken).toBeUndefined(); // No more pages

Pagination Options:

How It Works:

The mock automatically uses the last item from each page as the pagination token. This means:

AWS Error Simulation

Convenient helper methods for common AWS errors:

// S3 Errors
s3Mock.on(GetObjectCommand).rejectsWithNoSuchKey("missing-key");
s3Mock.on(GetObjectCommand).rejectsWithNoSuchBucket("missing-bucket");
s3Mock.on(GetObjectCommand).rejectsWithAccessDenied("protected-resource");

// DynamoDB Errors
dynamoMock.on(GetItemCommand).rejectsWithResourceNotFound("missing-table");
dynamoMock.on(PutItemCommand).rejectsWithConditionalCheckFailed();

// General AWS Errors
s3Mock.on(GetObjectCommand).rejectsWithThrottling();
s3Mock.on(GetObjectCommand).rejectsWithInternalServerError();

Delay/Latency Simulation

Simulate network delays for testing timeouts and race conditions:

// Resolve with delay
s3Mock.on(GetObjectCommand).resolvesWithDelay({ Body: "data" }, 1000);

// Reject with delay
s3Mock.on(GetObjectCommand).rejectsWithDelay("Network timeout", 500);

Fixture Loading

Load mock responses from files for easier test data management:

// Load JSON response from file (automatically parsed)
s3Mock.on(GetObjectCommand).resolvesFromFile("./fixtures/s3-response.json");

// Load text response from file (returned as string)
s3Mock.on(GetObjectCommand).resolvesFromFile("./fixtures/response.txt");

Debug Mode

Enable debug logging to see detailed information about mock configuration, lifecycle events, and command interactions:

const s3Mock = mockClient(S3Client);

// Enable debug logging
s3Mock.enableDebug();

// Configuration logs appear immediately:
// [aws-sdk-vitest-mock](Debug) Configured resolves for GetObjectCommand
// {
//   "matcher": {
//     "Bucket": "test-bucket"
//   },
//   "strict": false
// }
s3Mock
  .on(GetObjectCommand, { Bucket: "test-bucket" })
  .resolves({ Body: "data" });

// Interaction logs appear when commands are sent:
// [aws-sdk-vitest-mock](Debug) Received command: GetObjectCommand
// {
//   "Bucket": "test-bucket",
//   "Key": "file.txt"
// }
// [aws-sdk-vitest-mock](Debug) Found 1 mock(s) for GetObjectCommand
// [aws-sdk-vitest-mock](Debug) Using mock at index 0 for GetObjectCommand
await client.send(
  new GetObjectCommand({ Bucket: "test-bucket", Key: "file.txt" }),
);

// Lifecycle logs:
// [aws-sdk-vitest-mock](Debug) Clearing call history (mocks preserved)
s3Mock.reset();

// [aws-sdk-vitest-mock](Debug) Restoring original client behavior and clearing all mocks
s3Mock.restore();

// Disable debug logging
s3Mock.disableDebug();

Global Debug Configuration

Enable debug logging for all mocks globally, with the ability to override at the individual mock level:

import { setGlobalDebug, mockClient } from "aws-sdk-vitest-mock";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";

// Enable debug for all mocks
setGlobalDebug(true);

// All mocks will inherit the global debug setting
const s3Mock = mockClient(S3Client);
const dynamoMock = mockClient(DynamoDBClient);

// Both mocks will log debug information
s3Mock.on(GetObjectCommand).resolves({ Body: "data" });
dynamoMock.on(GetItemCommand).resolves({ Item: { id: { S: "1" } } });

// Override global setting for a specific mock
s3Mock.disableDebug(); // This mock won't log, but dynamoMock still will

// Disable global debug
setGlobalDebug(false);

Debug Priority (highest to lowest):

  1. Individual mock's enableDebug() or disableDebug() call (explicit override)
  2. Global debug setting via setGlobalDebug()
  3. Default: disabled

Key behaviors:

Debug mode provides comprehensive logging for:

Mock Configuration:

Mock Interactions:

Lifecycle Events:

๐Ÿงช Test Coverage

The library includes comprehensive test suites covering all features:

All utilities have dedicated test files ensuring reliability and maintainability.

๐Ÿงช Custom Matchers

Import the custom matchers in your test setup:

// vitest.setup.ts
import "aws-sdk-vitest-mock/vitest-setup";

Then use them in your tests:

import { expect, test } from "vitest";
import { mockClient } from "aws-sdk-vitest-mock";
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";

test("should call DynamoDB", async () => {
  const ddbMock = mockClient(DynamoDBClient);
  ddbMock.on(GetItemCommand).resolves({ Item: { id: { S: "123" } } });

  const client = new DynamoDBClient({});
  await client.send(
    new GetItemCommand({
      TableName: "users",
      Key: { id: { S: "123" } },
    }),
  );

  // Assert the command was called
  expect(ddbMock).toHaveReceivedCommand(GetItemCommand);

  // Assert it was called a specific number of times
  expect(ddbMock).toHaveReceivedCommandTimes(GetItemCommand, 1);

  // Assert it was called with specific input
  expect(ddbMock).toHaveReceivedCommandWith(GetItemCommand, {
    TableName: "users",
    Key: { id: { S: "123" } },
  });

  // Assert the nth call had specific input
  expect(ddbMock).toHaveReceivedNthCommandWith(1, GetItemCommand, {
    TableName: "users",
  });

  // Assert no other commands were received
  expect(ddbMock).toHaveReceivedNoOtherCommands([GetItemCommand]);
});

๐Ÿ“š API Reference

TypeScript documentation for this library can be found at here

mockClient<TClient>(ClientConstructor)

Creates a mock for an AWS SDK client constructor.

Returns: AwsClientStub<TClient>

mockClientInstance<TClient>(clientInstance)

Mocks an existing AWS SDK client instance.

Returns: AwsClientStub<TClient>

Global Debug Functions

AwsClientStub Methods

AwsCommandStub Methods (Chainable)

๐Ÿค Contributing

We welcome contributions! ๐ŸŽ‰ Please read our Contributing Guidelines for details on:

Quick Start for Contributors

# Fork and clone the repo
git clone https://github.com/YOUR-USERNAME/aws-sdk-vitest-mock.git
cd aws-sdk-vitest-mock

# Install dependencies
bun install

# Run tests
bun nx test

# Run linting
bun nx lint

# Build the library
bun nx build

# Make your changes and submit a PR!

See CONTRIBUTING.md for the complete guide.

Acknowledgements

This library is based on the core ideas and API patterns introduced by aws-sdk-client-mock, which is no longer actively maintained.

It reimagines those concepts for Vitest, while extending them with additional features, improved ergonomics, and ongoing maintenance.

๐Ÿ“ License

MIT


Made with โค๏ธ by sudokar

Join libs.tech

...and unlock some superpowers

GitHub

We won't share your data with anyone else.