I have been given a codebase built entirely in AWS Lambda (Using the serverless framework). I am trying now to setup a test suite in order to get to know the codebase and to be able to safely make changes. But Im having a hard time with AWS’s SDK for Javascript. It doesn’t seem to be able to be mocked.
What I need is to isolate the codebase from other AWS services like S3 and DynamoDB. Sinon and the (one and only) aws-sdk-mock packages doesn seem to work. I was wondering how did you structured your codebase to be able to be tested. Were you injecting an aws-sdk instance in each function? Were you even doing such testing locally?
Thank you
1 Like
I asked a similar question here
Yes I know. What did you get out of it?
Honestly speaking, if anything I found there are little to no examples of testing Serverless services with mocks for the AWS SDK. I still can’t figure out how to get mocks working with Jest ¯\_(ツ)_/¯
Here’s a thread I’ve started in order to fix this: https://github.com/serverless/examples/issues/158
2 Likes
What issues are you having with Sinon I’ve used it and it’s great for stubbing out the AWS calls?
Sinon doesn’t seem to work for stubbing another instance of requiring the aws-sdk
module.
const AWS = require('aws-sdk');
const DynamoDB = new AWS.DynamoDB({region: 'eu-central-1'})
module.exports.handler = function (event) {
return DynamoDB.get({...}).promise().then(...);
}
No matter what I tried, there is no way to mock the above. Even modules ilke aws-sdk-mock
says that the instantiation of the AWS module must be inside the handler.
Are you doing it differently?
Heya,
So it’s more about the structure I think, note I’m actually away from my laptop at the moment so this is from memory but should give a rough outline. Is this the kinda thing you wanted?
import expect from 'expect'
import sinon from 'sinon'
import myFunction from './myFunction'
const AWS = require('aws-sdk');
const DynamoDB = new AWS.DynamoDB({region: 'eu-central-1'})
describe('Some test', () => {
afterEach((done) => {
DynamoDB.get().restore() // This is just here for an example, so this will reset the originally dynamo function.
done()
})
it('Some scenario', () => {
sinon.stub(DynamoDB, 'get', () => {
return { someKey: 'someData'}
})
const event = { } // if you need to pass data into your function add it in here, the easiest way to get this is run a live request console.log out the event information and copy the structure into here.
const callback = (err, res) => {
expect(res.statusCode).toBe(200) // or whatever
expect(JSON.parse(res.body)).toEqual({ someKey: 'someData'})
done()
}
return myFunction(event, callback)
})
})
Thank you so much for helping out.
The above snippet won’t work. Plus, I don’t understand how stubbing the instance of the DynamoDB
you have created inside the test, will affect the instance inside the myFunction
module.
Thank you
Heya,
Because when you import myFunction, the function is the thing thats exported, so in myFunction after it’s imports it looks like this:
export default (event, callback) => {
If you think about it, we’re importing a function into this script, so it works in the test scope rather than it’s own. Also I as I remember multi requires(ie imports) will resolve back to the same object.
check this out: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/import
In short, it will work, try it!
To be clear this is how I set things up:
handler.js
import myFunction from './functions/myFunction/myFunction'
const functionHandler = (func) => {
return (event, context, callback) => {
func(event, context, callback)
}
}
const handler = {
myFunction: functionHandler(myFunction)
}
export default handler
Then when you want to add a new function you simply import it and add it to the handler object, this also means your functions are standalone and fit in with the above test layout.
1 Like
Hi @kbariotis
So I actually setup an entire services bootstrap framework that includes testing. It combines a number of node modules together to get the job done. Take a look at using a combination of mocha, chai, chai-as-promised and the main one serverless-mocha-plugin. What that last one does is allow you to pass whatever values you want into your function as the event parameter that would be passed from any AWS service, meaning mocking is a piece of cake. It sets up the lambda as a wrapped promise ready to be tested.
For example, in testing my validation function for my custom authorizer, the object passed into wrapped.run is a mocked event object:
const mod = require('../../functions/validate.js');
const mochaPlugin = require('serverless-mocha-plugin');
const lambdaWrapper = mochaPlugin.lambdaWrapper;
const expect = mochaPlugin.chai.expect;
const wrapped = lambdaWrapper.wrap(mod, {handler: 'validate'});
describe('validate Unit Test', () => {
it('Missing all required parameters', () => {
return wrapped.run({
type:"TOKEN",
authorizationToken:"bearer testtoken",
methodArn:"arn:aws:execute-api:<regionId>:<accountId>:<apiId>/<stage>/<method>/<resourcePath>"
}).then((response) => {
let expectedPolicyDocument = {
Version: '2012-10-17',
Statement:
[ { Action: 'execute-api:Invoke',
Effect: 'Deny',
Resource: 'arn:aws:execute-api:<regionId>:<accountId>:<apiId>/<stage>/<method>/<resourcePath>' } ]
};
expect(response.policyDocument).to.exist;
expect(response.policyDocument).to.be.eql(expectedPolicyDocument);
});
});
2 Likes
I used mockery and it helped me. Here is an example:
let assert = require(“chai”).assert;
let mockery = require(‘mockery’);
let sinon = require(‘sinon’);
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false
});
let deleteItemSpy = sinon.spy();
let querySpy = sinon.spy();
mockery.registerMock(‘dynamodb-doc’, {
DynamoDB: function () {
this.deleteItem = deleteItemSpy;
this.query = querySpy;
}
});
let handler = require(’…/handler’);
let fakeRequest = {
body: ‘{}’,
requestContext: {
authorizer: {claims: {‘cognito:username’: ‘test’, email: ‘test@gmail.com’}},
},
queryStringParameters: {id: ‘1’}
};
describe(“Invitations table related methods:”, function () {
it(“DELETE method:”, (done) => {
fakeRequest.httpMethod = ‘DELETE’;
handler.invitations(fakeRequest);
sinon.assert.calledWith(deleteItemSpy, sinon.match({
ConditionExpression: “createdBy = :createdBy”,
ExpressionAttributeValues: {’:createdBy’: ‘test’},
Key: {id: ‘1’},
TableName: ‘invitations’
}));
done();
});
it("GET method:", (done) => {
fakeRequest.httpMethod = 'GET';
handler.invitations(fakeRequest);
sinon.assert.calledWith(querySpy, sinon.match({
ExpressionAttributeNames: {'#id': "id"},
ExpressionAttributeValues: {':id': "1"},
KeyConditionExpression: "#id = :id",
TableName: "invitations"
}));
done();
});
});