AWS Developer Tools Blog

Mocking modular AWS SDK for JavaScript (v3) in Unit Tests

The AWS SDK for Javascript team would like to highlight the open-source community and it’s contributions. Today we welcome a guest blog written by Maciej Radzikowski on aws-sdk-client-mock, a library that allows easy mocking of AWS SDK for JavaScript (v3).

On December 15th, 2020, AWS announced the general availability of the AWS SDK for JavaScript, version 3 (v3). In order to test your code behavior thoroughly when using the SDK, it must be easy to mock in unit tests. This blog post shows how you can mock the SDK clients using the community-driven AWS SDK Client mock library.

Background

Spending too much time on building mocks instead of writing unit tests slows down the development process.

For the AWS SDK for JavaScript (v2), you can use the aws-sdk-mock library for unit test mocks. It is built by the community and allows you to execute your custom logic for the SDK method calls. I needed something similar for the AWS SDK for JavaScript (v3).

I wanted to switch to the AWS SDK for JavaScript (v3) from the moment it became generally available, and retain the high unit tests coverage at the same time. That’s why I created the AWS SDK Client mock which is a specialized solution to mock AWS SDK for JavaScript (v3) in a unified, straightforward, and readable way.

Usage

The mocking library for AWS SDK for JavaScript (v3) can be used in any JavaScript unit testing framework. Internally, it uses Sinon.JS to create robust stubs in place of the SDK calls. You can install it with your favorite package manager.

In the following code, we use npm:

npm install --save-dev aws-sdk-client-mock

Example code to be tested

We have a function that takes an array of user IDs, finds the user information for each user ID from DynamoDB using DynamoDB Document Client, and returns an array of corresponding user names:

// getUserNames.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";

export const getUserNames = async (userIds: string[]) => {
  const dynamodb = new DynamoDBClient({});
  const ddb = DynamoDBDocumentClient.from(dynamodb);
  const names = [];
  for (const userId of userIds) {
    const result = await ddb.send(
      new GetCommand({
        TableName: "users",
        Key: {
          id: userId,
        },
      })
    );
    names.push(result.Item?.name);
  }
  return names;
};

We can optimize this code using a BatchGetCommand instead of the GetCommand. But we keep it short and simple to focus on our tests.

Example code for unit testing

All the examples below use Jest as a testing framework. The mocks and presented ideas will work the same way in any other testing framework.

In your unit test file, you need to import aws-sdk-client-mock and the client to be tested. In the below example, we import DynamoDB Document Client.

import { mockClient } from "aws-sdk-client-mock";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";

In AWS SDK for JavaScript (v3), all calls to operations are made using the Client class objects. You need to pass the class of the client you want to mock. We start with creating the mock instance as follows:

const ddbMock = mockClient(DynamoDBDocumentClient);

After every test execution, you need to reset the history and behavior of the mock. Otherwise, the tests could interfere with each other. You can achieve this by resetting the mock before each individual test:

beforeEach(() => {
  ddbMock.reset();
});

Now you write your first test as follows:

// getUserNames.spec.ts
import { getUserNames } from "./getUserNames";
import { GetCommand } from "@aws-sdk/lib-dynamodb";

it("should get user names from the DynamoDB", async () => {
  ddbMock.on(GetCommand).resolves({
    Item: { id: "user1", name: "John" },
  });
  const names = await getUserNames(["user1"]);
  expect(names).toStrictEqual(["John"]);
});

In the first line of the unit test, we specify the mock behavior. When called with the GetCommand, it will resolve with the specified object containing our user data. Then we call the function under test and check if the response matches the value returned from the mock.

In the above example, the mock returns a user object for any GetCommand call. We can expand the test by implementing different responses depending on command parameters as follows:

import { GetCommand } from "@aws-sdk/lib-dynamodb";

it("should get user names from the DynamoDB", async () => {
  ddbMock
    .on(GetCommand)
    .resolves({
      Item: undefined,
    })
    .on(GetCommand, {
      TableName: "users",
      Key: { id: "user1" },
    })
    .resolves({
      Item: { id: "user1", name: "Alice" },
    })
    .on(GetCommand, {
      TableName: "users",
      Key: { id: "user2" },
    })
    .resolves({
      Item: { id: "user2", name: "Bob" },
    });
  const names = await getUserNames(["user1", "user2", "user3"]);
  expect(names).toStrictEqual(["Alice", "Bob", undefined]);
});

In the unit test above, we define the default behavior for the GetCommand operation, resolving with an undefined value for Item. After that, we defined more specific behaviors in which the name Alice is returned for user ID user1 and the name Bob is returned for user ID user2.

You can git clone aws-samples/aws-sdk-js-client-mock-test and run these unit tests in your environment.

Built-in type checking

Just like AWS SDK for JavaScript (v3), the mocks are also fully typed. When using TypeScript, the output object you provide is statically checked against the expected output type for the GetCommand.

For example, update the mock to return boolean true instead of Item object as follows:

ddbMock.on(GetCommand).resolves(true);

TypeScript will throw the following error informing you that a boolean value can’t be returned as a output from GetCommand:

Argument of type 'boolean' is not assignable to parameter of
type 'CommandResponse<GetCommandOutput>'.

The types in the unit test mocks help you write unit tests faster thanks to code suggestions and completion in your IDE and avoid mistakes like typos in the property names.

Further reading

The mocking library also supports mocking for more complex operations, like:

  •  Specifying reject results (errors) from Client calls.
  •  Providing a custom function invoked for the incoming calls.
  •  Inspecting received calls instead of returning the predeclared response.

You can find all of them described with examples in the repository and API reference.

Community and Development

The AWS SDK Client mock library is an open-source project under the MIT license. It is compatible with all the AWS SDK for JavaScript (v3) Clients. As it’s community-driven, there are several ways you can help:

As of now, the aws-sdk-client-mock library enables mocking of only the most essential SDK operation, i.e. sending Commands. Other capabilities and improvements will likely be added over time. Upon stumbling on the library, the AWS SDK for JavaScript team provided help to make it work seamlessly with the SDK itself and I’m very thankful for that.

Conclusion

The AWS SDK for JavaScript team recommends AWS SDK Client mock library for mocking the AWS SDK for JavaScript (v3). Give it a try to make your code reliable, keeping your tests straightforward and easily readable. If you have any feedback, bug reports, or feature requests, please open an issue on GitHub.