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)
};
Okay so the way I would approach this is as follows:
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');
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.
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");
});
});
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).
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,
});
});
});