classjestjsaws-sdk

How can I mock aws-sdk with jest?


I am trying to mock aws-sdk with jest. Actually I only care about one function. How can I do this? I have read the docs about mocking classes with jest, but the docs are complicated and I don't quite understand them.

Here is my best attempt:

handler.test.js

'use strict';

const aws = require('aws-sdk');
const { handler } = require('../../src/rotateSecret/index');

jest.mock('aws-sdk');

const event = {
  SecretId: 'test',
  ClientRequestToken: 'ccc',
  Step: 'createSecret',
};

describe('rotateSecret', () => {
  it.only('should not get or put a secret', async () => {
    aws.SecretsManager.mockImplementation(() => ({
      getSecretValue: () => ({}),
    }));
    expect.assertions(1);

    await handler(event);

    // You can see what I am trying to do here but it doesn't work
    expect(aws.SecretsManager.getSecretManager).not.toHaveBeenCalled();
  });
});

handler.js

exports.handler = async (event) => {
  const secretsManager = new aws.SecretsManager();
  const secret = await secretsManager.describeSecret({ SecretId: event.SecretId }).promise();

  if (someCondition) {
    console.log("All conditions not met");
    return;
  }

  return secretsManager.getSecretValue(someParams)
};


Solution

  • Okay so the way I would approach this is as follows:

    AWS-SDK Mock


    Create an actual mock for aws-sdk and put it in __mocks__/aws-sdk.js file at the root of your project

    // __mocks__/aws-sdk.js
    
    class AWS {
      static SecretsManager = class {
        describeSecret = jest.fn(() => {
          return {promise: () => Promise.resolve({ARN: "custom-arn1", Name: "describeSec"})}
        });
        getSecretValue = jest.fn(() => {
          return {
            promise: () => Promise.resolve({ARN: "custom-arn2", Name: "getSecretVal"})
          };
        });
      }
    }
    
    module.exports = AWS;
    

    I have used static before SecretsManager because AWS class is never instantiated yet it wants access to SecretsManager class.

    Inside SecretsManager, I have defined 2 functions and stubbed them using jest.fn.

    Now same stuff as you have done in your test file:

    jest.mock('aws-sdk');
    

    How to Test


    To test if your mock functions are called, thats the tricky part (so i will detail that at the end of this post).

    Better approach would be to assert against the end result of your main function after all processing is finished.

    Assertions


    Back in your test file, I would simply invoke the handler with the await (as you already have) and then assert against the final result like so:

    // test.js
    
    describe("rotateSecret", () => {
      it.only("should not get or put a secret", async () => {
        const event = {name:"event"};
        const result = await handler(event);
        expect(result).toEqual("whatever-your-function-is-expected-to-return");
      });
    });
    

    Testing Secret Manager's function invocations


    For this you will need to tweak your main handler.js file itself and will need to take out invocation of secrets Manager from the main function body like so:

    const secretsManager = new aws.SecretsManager(); // <---- Declare it in outer scope
    
    exports.handler = async (event) => {
      const secret = await secretsManager
        .describeSecret({ SecretId: event.SecretId })
        .promise();
    
      if (someCondition) {
        console.log("All conditions not met");
        return;
      }
    
      return secretsManager.getSecretValue(someParams);
    };
    

    Then back in your test.js file, you will need to similarly declare the SecretsManager invocation before you initiate your handler function like so:

    //test.js
    
    describe("rotateSecret", () => {
      const secretsManager = new aws.SecretsManager(); // <---- Declare it in outer scope
    
      it.only("should not get or put a secret", async () => {
        const event = {name:"event"};
    
        await handler(event);
    
        // Now you can make assertions on function invocations
        expect(secretsManager.describeSecret).toHaveBeenCalled();
    
        // OR check if passed args were correct
        expect(secretsManager.describeSecret).toHaveBeenCalledWith({
          SecretId: event.SecretId,
        });
      });
    });
    

    This will allow you to make assertions on function invocation as well the args that were passed.

    The reason I declare it outside function scope is to tell Jest that secretsManager should be existing somewhere in global scope and it should be used from there.

    Previously, we had it declared inside the function scope, so Jest would invoke it but we weren't able to get access to it.

    We couldn't directly reference it like this AWS.SecretsManager.getSecretManager because getSecretManager method is only available after you instantiate the SecretsManager class (and even if you did that, you will get a new instance of the class which won't help with any assertions).

    Downside of __mocks__/aws.js fake module


    Obvious issue is - you are stubbing the function on every single call and maybe you won't want that.

    Perhaps you only want to stub it out once for a specific test but for the rest of them you want it to run normal.

    In that case, you should not create __mocks__ folder.

    Instead, create a one-time fake BUT make sure your SecretsManager invocation is in the outside scope in your test file as before.

    //test.js
    const aws = require("aws-sdk");
    
    describe("rotateSecret", () => {
     // Declare it in outer scope
      const secretsManager = new aws.SecretsManager();
    
      it.only("should not get or put a secret", async () => {
        const event = {name:"event"};
    
        // Create a mock for this instance ONLY
        secretsManager.describeSecret = jest.fn().mockImplementationOnce(()=>Promise.resolve("fake-values"));
    
        await handler(event);
        expect(secretsManager.describeSecret).toHaveBeenCalled();
        expect(secretsManager.describeSecret).toHaveBeenCalledWith({
          SecretId: event.SecretId,
        });
      });
    });