AWS SNS: Mock sns.publish with Jest
Outline
There are a lot of jest mock examples out there, but never a clear example about Jest with AWS services (in this case, SNS). Today, in this article I will show you how to mock sns.publish
with Jest and with all the code layed out and the logic explained.
1. Understand what you’re testing
Let’s take a look at the Node.js code that we want to write our unit test for. Inside sendMessage.js
is the handler()
function for the AWS Lamda that is responsible to send message to our AWS SNS whenever triggered. How this Lambda function gets triggered doesn’t matter here. All it matters is that this Lambda function will send message to our target SNS.
Inside sendMessage.js
:
const AWS = require('aws-sdk')
const sns = new AWS.SNS()
// this handler sends message to SNS whenever called upon
const handler = async event => {
if event.status === "send" {
try {
await sns.publish({
Message: "send this message to SNS..."
}).promise()
} catch (err) {
throw err
}
}
}
module.exports = {
handler
}
This is a fairly basic example, but enough to help us demonstrate how to mock sns.publish
.
Inside handler()
function, it takes in an input event
and checks if event.status
equals to “send”. If equals to “send” then send the hard coded message “send this message to SNS…” to our target SNS.
Now, let’s try and mock this Lambda handler()
function that publishes message to SNS!
2. How to write mock for sns.publish
using Jest
We will be using Jest to write our unit test and that’s it. No fancy thrid party packages needed. First, you need to create a new file named sendMessage.test.js
that will contain our tests.
Inside sendMessage.test.js
:
describe('sendMessage handler behaviors', () => {
let mockSNS
beforeAll(() => {
// things you want to do before all the tests starts
})
afterAll(() => {
// things you want to do after all the tests ends
})
beforeEach(() => {
jest.resetModules()
const AWS = require('aws-sdk')
mockSNS = {
publish: jest.fn().mockReturnValue({
promise: jest.fn().mockResolvedValue({})
})
}
jest.spyOn(AWS, 'SNS').mockReturnValue(mockSNS)
})
test('should send message to SNS', async () => {
const handler = require('./sendMessage')
await handler(triggerInput("send"))
expect(mockSNS.publish).toHaveBeenCalledTimes(1)
expect(mockSNS.publish).toHaveBeenCalledWith(expectedPublishContent)
})
const triggerInput = (status) => {
status: status
}
const expectedPublishContent = () => {
Message: "send this message to SNS..."
}
})
Ok, let’s break down this unit test code:
-
describe()
: the testing function that we use to define our test cases and behaviors that will be carried out at different stages. This is not a required step, you can skipdescribe()
and writetest()
instead. But when there are a lot of tests, it would be best to organize your test cases with describe(). -
beforeAll()
: define things you want to do before all the tests starts. -
afterAll()
: define things you want to do after all the tests ends. -
beforeEach()
: define things to do before eachtest()
starts.-
jest.resetModules()
: this resets the module registry that stores cache. This step is useful when you want to make sure that each time your test starts with a clean state. -
jest.spyOn(AWS, 'SNS')
: creates a mock function likejest.fn
that tracks calls coming intoAWS.SNS()
, and returns a mock function. Everytime before each test case, we will mockAWS.SNS()
with this spyOn function. Also, remember that theconst AWS = require('aws-sdk')
should be defined insidebeforeEach()
, because we want a newAWS
for each test case and not a global one (don’t requireaws-sdk
module outside ofbeforeEach()
). -
.mockReturnValue(mockSNS)
: meaning that our mock function created withjest.spyOn(AWS, 'SNS')
will returnmockSNS
it’s return value.
-
-
test()
: the specific test case that we want thedescribe()
function to run. Insidetest('should send message to SNS)
, it will requiresendMessage.js
and call thehandler()
that was previously defined.-
expect(mockSNS.publish).toHaveBeenCalledTimes(1)
: This checks if our mocked sns.publish was called once. If called once, then retirn true. In our case, it should be true, sincehandler(triggerInput("send"))
does receice an event withevent.status
equals to “send”, and so our lambda function should sns.publish the message we want. -
expect(mockSNS.publish).toHaveBeenCalledWith(expectedPublishContent)
: this checks if the correct message was called. This basically checks if the correct message was send throughsns.publish
. In this case, the correct message is defined insideexpectedPublishContent
. This expect function will check if the message we send to mock sns is the same as what’s insideexpectedPublishContent
. If the same, then return true.
-
3. Closer look at mockSNS.publish.mock
Every mock function in Jest have a .mock
property that stores the information about how the mock function was called. The same goes with our mockSNS.publish
. You must be really curious about what is actually inside mockSNS.publish.mock
right? Well, below is an example of the object inside mockSNS.publish.mock
after running one of the test and the call have been catched by our jest.SpyOn
function.
# console.log(mockSNS.publish.mock)
{
"calls": [
[
{
"Message": "send this message to SNS..."
}
]
],
"instances": [
{}
],
"invocationCallOrder": [
1
],
"results": [
{
"type": "return",
"value": {}
}
]
}
-
calls
: an array of all the calls this mock function received-
calls[0]
: first call to the mock function -
calls[0][0]
: first arg of the first call of the mock function
-
-
instances
: an array of objects returned by each instantiation of the mock function-
instances[0]
: the first instantiation of our mock function -
In this case, our returned object when instantiating the mock function is empty.
-
-
invocationCallOrder
: an array of the order of each call made to the mock function.-
invocationCallOrder[0]
: is the first call’s call order -
Our call order is 1, because our first and only call was the first to call the mock function.
-
-
results
: an array of the return values by the mock function when each call was called-
results[0]
: return value from the first call -
In our case, the return value for the first call is an empty object.
-