Jest: Mocking With AWS SDK V3

Jest: Mocking With AWS SDK V3

Mocking made quick and easy when using the new modular components from @aws-sdk V3 client libraries

ยท

8 min read

The recent V3 update to AWS's JavaScript SDK has introduced a number of benefits to developers, especially when it comes to bundle sizes and modularity. No longer are the days of having to import all operations related to DynamoDB just to put an item into a database. However, in AWS's refactored solution, it has become less clear to some how to properly mock these new modules. This quick article hopes to alleviate this issue, showing one way that makes mocking @aws-sdk client libraries a breeze.

๐Ÿ›  Setting up the Test Environment

๐Ÿ“ Note: The aim of this article is to be a quick demo of mocking the @aws-sdk, not a Jest integration overview. As such, I will not be going into the internals of Jest. You can do that here.

Before starting, make sure that you have Jest and any AWS modules that you plan to test with, installed. In this article, I will be using the DynamoDB client, however this works with any clients that make use of the send() function for their operations.

npm install jest @aws-sdk/client-dynamodb

As standard, add the following to your package.json file

scripts: {
    # Rest of your scripts go here...
    "test": "jest",
}

As a demo, I will setup a file which I wish to test: dynamoDBController.js. For simplicity, this file will only contain one method: PutItem(). The base code for this is as follows:

// dynamoDBController.js

const { 
  DynamoDBClient, 
  PutItemCommand 
} = require("@aws-sdk/client-dynamodb");

const dynamoDBClient = new DynamoDBClient({ region: "local" });

const PutItem = async (params) => {
  try {
    return await dynamoDBClient.send(new PutItemCommand(params));
  } catch (error) {
    return { statusCode: 500 };
  }
};

module.exports = { PutItem };

๐Ÿ›  Enabling Mocking of AWS Modules

Once setup, one minor refactor to the existing code unlocks the ease of mocking. By refactoring the client initialization into a separate file, it becomes easy to mock the AWS module. In the current example, I will add a file aws.js to setup the DynamoDB client:

// aws.js

const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");

const dynamoDBClient = new DynamoDBClient({
  region: "local",
});

module.exports = { dynamoDBClient };

And the minor adjustment to dynamoDBController.js:

// updated dynamoDBController.js

const { PutItemCommand } = require("@aws-sdk/client-dynamodb");
const { dynamoDBClient } = require("./aws");

const PutItem = async (params) => {
  try {
    return await dynamoDBClient.send(new PutItemCommand(params));
  } catch (error) {
    return { statusCode: 500 };
  }
};

module.exports = { PutItem };

This small change separates the external dependency initialization from the actual usage of the module, breaking up this operation into separate units, making it easier to mock and test.

๐Ÿ›  Mocking and Testing AWS Modules

Now that the DynamoDB module has been setup for mocking, it's time to create the test file. In a new dynamoDBController.test.js file, import both the local method to test as well as the initialized dynamoDBClient:

// dynamoDBController.test.js

const { PutItem } = require("./dynamoDBController");
const { dynamoDBClient } = require("./aws");

Using the Jest mock function, we can add the following line to mock DynamoDB module:

jest.mock("./aws.js");

Now we can move onto writing the actual tests. As we are mocking the initialized client, we have access to the exposed client send() function, allowing us to intercept the fundamental DynamoDB call used within the PutItem() function. Within our test, we can intercept this call and return our own test value, by using the following command:

dynamoDBClient.send.mockResolvedValue({ isMock: true });

We can now write the rest of the test and call the PutItem() command as normal. The rest of the test is as follows:

describe("@aws-sdk/client-dynamodb mock", () => {
  it("should successfully mock dynamoDBClient", async () => {
    dynamoDBClient.send.mockResolvedValue({ isMock: true });

    const params = {
      TableName: "CUSTOMER",
      Item: {
        CUSTOMER_ID: { N: "001" },
        CUSTOMER_NAME: { S: "Richard Roe" },
      },
    };

    const response = await PutItem(params);

    expect(response.isMock).toEqual(true);
  });
});

Running npm test now will result in a successful test run.

> jest

 PASS  ./dynamoDBController.test.js
  @aws-sdk/client-dynamodb mock
    โˆš should successfully mock dynamoDBClient (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.307 s, estimated 3 s
Ran all test suites.

As the PutItem() function also accounts for and thrown errors, it is also possible to fully test this by using the mockRejectedValue() function. A test demonstrating this is documented below:

  it("should throw error and return 500 status code", async () => {
    dynamoDBClient.send.mockRejectedValue(new Error("Async error"));

    const params = {
      TableName: "CUSTOMER",
      Item: {
        CUSTOMER_ID: { N: "001" },
        CUSTOMER_NAME: { S: "Richard Roe" },
      },
    };

    const response = await PutItem(params);

    expect(response.statusCode).toEqual(500);
  });

Running npm test now will yield two successful tests:

> jest

 PASS  ./dynamoDBController.test.js
  @aws-sdk/client-dynamodb mock
    โˆš should successfully mock dynamoDBClient (2 ms)
    โˆš should throw error and return 500 status code (2 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        3.529 s
Ran all test suites.

๐Ÿ›  Preventing Race Conditions and Mock Data Leakage in your Tests

If your tests are sending multiple call to the mocked AWS modules, it is good practice to add a Jest call to the beforeEach() so that any mocked return values can be reset before each test. This lets you dynamically code different responses for each individual test, as well as preventing test data leakage and race conditions. To do this, simply append the following snippet before the start of your test blocks:

beforeEach(() => {
  dynamoDBClient.send.mockReset();
});

๐Ÿค” Conclusion

The V3 update to the AWS JavaScript SDK is great. It reduces bundle sizes and increases modularity. Upgrading to it is great for developers and packages. Refactoring existing tests for methods moving this new major version does not have to be hard, especially when you embrace the modular first approach of the new SDK.