nodejs: How to stub AWS s3 getobject with sinon - node.js

I am trying to write unit tests but am having trouble stubbing the AWS S3 getobject method. Here is my code:
describe('testExecuteSuccess()', function () {
it('Test execution with id is successful.', async () => {
let id = "test_id";
const getObjectStub = AWS.S3.prototype.getObject = sinon.stub();
getObjectStub.returns({ promise: () => Promise.resolve({ Body: "test" }) });
await executionHandler.execute(id).then(() => {
getObjectStub.should.have.been.calledOnce;
});
});
});
Does anyone know what is wrong with it/how to properly stub the getObject method? When I run the test, I am getting InvalidParameterType: Expected params.Bucket to be a string which proves that the stub is not working.
Here is the code to the executionHandler.execute method:
exports.execute = async function(curr_id) {
let params = { Bucket: BUCKET_NAME, Key: KEY_PATH }
let fileObject = await s3.getObject(params).promise();
let codeId = executeCode(fileObject).id;
if (codeId !== curr_id) {
throw "Id from executed code does not match currentId: " + curr_id;
}
}
Note: I am using Mocha test runner.

You can stub out dependencies such as aws-sdk module with link seams. This is the CommonJS version, so we will be using proxyquire to construct our seams.
unit test solution:
main.js:
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.execute = async function (curr_id) {
const BUCKET_NAME = 'examplebucket';
const KEY_PATH = 'SampleFile.txt';
let params = { Bucket: BUCKET_NAME, Key: KEY_PATH };
let fileObject = await s3.getObject(params).promise();
let codeId = executeCode(fileObject).id;
if (codeId !== curr_id) {
throw 'Id from executed code does not match currentId: ' + curr_id;
}
};
function executeCode(file) {
return file;
}
main.test.js:
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const { expect } = chai;
describe('51959840', () => {
afterEach(() => {
sinon.restore();
});
it('should do nothing if codeId is equal with curr_id', async () => {
const fileObject = { id: '1' };
const s3Mock = {
getObject: sinon.stub().returnsThis(),
promise: sinon.stub().resolves(fileObject),
};
const AWSMock = { S3: sinon.stub().returns(s3Mock) };
const { execute } = proxyquire('./main', {
'aws-sdk': AWSMock,
});
await execute('1');
sinon.assert.calledOnce(AWSMock.S3);
sinon.assert.calledWithExactly(s3Mock.getObject, { Bucket: 'examplebucket', Key: 'SampleFile.txt' });
sinon.assert.calledOnce(s3Mock.promise);
});
it('should throw error if codeId is NOT equal with curr_id', async () => {
const fileObject = { id: '2' };
const s3Mock = {
getObject: sinon.stub().returnsThis(),
promise: sinon.stub().resolves(fileObject),
};
const AWSMock = { S3: sinon.stub().returns(s3Mock) };
const { execute } = proxyquire('./main', {
'aws-sdk': AWSMock,
});
await expect(execute('1')).to.rejectedWith('Id from executed code does not match currentId: 1');
sinon.assert.calledOnce(AWSMock.S3);
sinon.assert.calledWithExactly(s3Mock.getObject, { Bucket: 'examplebucket', Key: 'SampleFile.txt' });
sinon.assert.calledOnce(s3Mock.promise);
});
});
unit test result:
51959840
✓ should do nothing if codeId is equal with curr_id (2884ms)
✓ should throw error if codeId is NOT equal with curr_id
2 passing (3s)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
main.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
package versions:
"proxyquire": "^2.1.3",
"sinon": "^8.1.1",
"aws-sdk": "^2.817.0",

Related

TypeError: AWS.SecretsManager is not a constructor in unit testing with proxyquire

I have written a test code to test code that gives credentials from AWS Secret Manager. I used proxyquire and sinon for stubbing and getting this error.
Function I want to test
exports.getCredsFromAWSSecretsManager = (keyName) => {
const SM = new AWS.SecretsManager({
apiVersion: process.env.AWS_SM_API_VERSION,
region: process.env.AWS_SM_REGION
});
return SM.getSecretValue(params).promise().then((data) => {
logger.info(logMsgs.awsHlpr_smGetSecretValueSuccess(JSON.stringify(data)));
return JSON.parse(data.SecretString);
}).catch((err) => {
logger.error(logMsgs.awsHlpr_smGetSecretValueErr(JSON.stringify(err)));
throw err;
});
};
Test case that I have written
const sinon = require("sinon");
const proxyquire = require("proxyquire").noCallThru().noPreserveCache();
const { mockLogger } = require("../../mockdata/mockLogger");
let awsHelper;
let secretsManagerStub;
describe.only("AWS Helper ", () => {
// function1
describe("AWS Helper: getCredsFromAWSSecretsManagera method", () => {
before((done) => {
const data = {
SecretString: JSON.stringify({ publicKey: 'secretUsername', privateKey: 'secretPassword' }),
};
secretsManagerStub = {
getSecretValue: sinon.stub().callsFake((params, callback) => {
callback(null, data);
}),
};
const awsStub = {
SecretsManager: sinon.stub().returns(secretsManagerStub)
}
awsHelper = proxyquire('../../../utils/aws_helper.js', {
'aws-sdk':{
AWS:awsStub
} ,
"../../utils/logger": mockLogger,
});
done();
});
afterEach(() => {
sinon.restore();
});
it('should write random data!', async () => {
const expectedData = "abcdef";
secretsManagerStub.getSecretValue.yields(null, expectedData);
const data = await awsHelper.getCredsFromAWSSecretsManager();
sinon.assert.callCount(secretsManagerStub.getSecretValue, 1);
assert.strictEqual(data, expectedData);
});
});
});
This code gives me the error saying
TypeError: AWS.SecretsManager is not a constructor
any help would be greatly appreciated.
AWS is a namespace, it contains all AWS service classes like SecretsManager. You should provide the awsStub to aws-sdk, there is no need to wrap the awsStub inside an object.
aws_helper.js:
const AWS = require('aws-sdk');
exports.getCredsFromAWSSecretsManager = () => {
const SM = new AWS.SecretsManager({
apiVersion: process.env.AWS_SM_API_VERSION,
region: process.env.AWS_SM_REGION,
});
const params = {
SecretId: '1',
};
return SM.getSecretValue(params)
.promise()
.then((data) => {
console.info(data);
return JSON.parse(data.SecretString);
})
.catch((err) => {
console.error(err);
throw err;
});
};
aws_helper.test.js:
const sinon = require('sinon');
const proxyquire = require('proxyquire').noCallThru().noPreserveCache();
let awsHelper;
let secretsManagerStub;
describe('AWS Helper: getCredsFromAWSSecretsManagera method', () => {
before(() => {
const data = {
SecretString: JSON.stringify({ publicKey: 'secretUsername', privateKey: 'secretPassword' }),
};
secretsManagerStub = {
getSecretValue: sinon.stub().returnsThis(),
promise: sinon.stub().resolves(data),
};
const awsStub = {
SecretsManager: sinon.stub().returns(secretsManagerStub),
};
awsHelper = proxyquire('./aws_helper.js', {
'aws-sdk': awsStub,
});
});
afterEach(() => {
sinon.restore();
});
it('should write random data!', async () => {
const data = await awsHelper.getCredsFromAWSSecretsManager();
sinon.assert.callCount(secretsManagerStub.getSecretValue, 1);
sinon.assert.match(data, { publicKey: 'secretUsername', privateKey: 'secretPassword' });
});
});
test result:
AWS Helper: getCredsFromAWSSecretsManagera method
{
SecretString: '{"publicKey":"secretUsername","privateKey":"secretPassword"}'
}
✓ should write random data!
1 passing (2s)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 77.78 | 100 | 66.67 | 77.78 |
aws_helper.js | 77.78 | 100 | 66.67 | 77.78 | 19-20
---------------|---------|----------|---------|---------|-------------------

How to mockup a constant defined in another file using sinon and rewire?

I'm a beginner with JS tests and I'm having issues when I try to mockup the value of a constant in the file I need to test.
I have the following file
// index.js
const { MultiAccounts } = require('../../some.js')
const MultiAccountInstance = new MultiAccounts();
...
const syncEvents = () => Promise.try(() => {
// ...
return MultiAccountInstance.all()
.then((accounts) => // ...); // ==> it throws the exception here Cannot read property 'then' of undefined
});
module.exports = syncEvents;
So, I will like to mockup the MultiAccountInstance constant.
I had been trying using Simon and rewire, but with the following script I'm having it throws the exception here Cannot read property 'then' of undefined exception in the script above.
//index.test.js
const rewire = require('rewire');
const indexRewired = rewire('.../../index/js');
describe('testing sync events', () => {
let fakeMultiAccountInstance, MultiAccountInstanceReverter;
let accounts;
beforeEach(() => {
accounts = [{id: 1}, {id: 2}];
fakeMultiAccountInstance = {};
fakeMultiAccountInstance.all = () => Promise.resolve(accounts);
MultiAccountInstanceReverter = indexRewired.__set__('MultiAccountInstance', fakeMultiAccountInstance);
});
afterEach(() => {
MultiAccountInstanceReverter();
});
it('testing', ()=> {
const spy = sinon.stub(fakeMultiAccountInstance, 'all');
return indexRewired().then((resp) => {
spy.restore();
expect(spy).to.have.been.calledWith({someParams: true});
});
})
});
How can I achieve this?. I also tried using stubs, but I'm having the error that the MultiAccountInstance.all is not a function
it's something like this
//index.test.js
const rewire = require('rewire');
const indexRewired = rewire('.../../index/js');
describe('testing sync events', () => {
let stubMultiAccountInstance, MultiAccountInstanceReverter;
let accounts;
beforeEach(() => {
accounts = [{id: 1}, {id: 2}];
stubMultiAccountInstance= sinon.stub().returns({
all: () => Promise.resolve(accounts), // also tried with sinon.stub().resolves(accounts)
});
MultiAccountInstanceReverter = indexRewired.__set__('MultiAccountInstance', stubMultiAccountInstance);
});
afterEach(() => {
stubMultiAccountInstance.reset();
MultiAccountInstanceReverter();
});
it('testing', ()=> {
return indexRewired().then((resp) => {
expect(stubMultiAccountInstance).to.have.been.calledWith({someParams: true});
});
})
});
Do you know what am I doing wrong?
Here is the unit test solution:
index.js:
const { MultiAccounts } = require('./some.js');
const Promise = require('bluebird');
let MultiAccountInstance = new MultiAccounts();
const syncEvents = () =>
Promise.try(() => {
return MultiAccountInstance.all().then((accounts) => console.log(accounts));
});
module.exports = syncEvents;
some.js:
function MultiAccounts() {
async function all() {}
return {
all,
};
}
module.exports = { MultiAccounts };
index.test.js:
const sinon = require('sinon');
const rewire = require('rewire');
const Promise = require('bluebird');
describe('61659908', () => {
afterEach(() => {
sinon.restore();
});
it('should pass', async () => {
const promiseTrySpy = sinon.spy(Promise, 'try');
const logSpy = sinon.spy(console, 'log');
const indexRewired = rewire('./');
const accounts = [{ id: 1 }, { id: 2 }];
const fakeMultiAccountInstance = {
all: sinon.stub().resolves(accounts),
};
indexRewired.__set__('MultiAccountInstance', fakeMultiAccountInstance);
await indexRewired();
sinon.assert.calledOnce(fakeMultiAccountInstance.all);
sinon.assert.calledWith(logSpy, [{ id: 1 }, { id: 2 }]);
sinon.assert.calledOnce(promiseTrySpy);
});
});
unit test results with coverage report:
61659908
[ { id: 1 }, { id: 2 } ]
✓ should pass (54ms)
1 passing (62ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 80 | 100 |
index.js | 100 | 100 | 100 | 100 |
some.js | 100 | 100 | 50 | 100 |
----------|---------|----------|---------|---------|-------------------

sinon stub for Lambda using promises

I just started using sinon, and I had some initial success stubbing out DynamoDB calls:
sandbox = sinon.createSandbox()
update_stub = sandbox.stub(AWS.DynamoDB.DocumentClient.prototype, 'update').returns({
promise: () => Promise.resolve(update_meeting_result)
})
This works great.
But I also need to stub Lambda, and the same approach isn't working:
lambda_stub = sandbox.stub(AWS.Lambda.prototype, 'invoke').returns({
promise: () => Promise.resolve({lambda_invoke_result}) //
})
With this, I get the error: Cannot stub non-existent property invoke.
example implementation:
const AWS = require("aws-sdk")
AWS.config.update({region: 'us-west-2'})
const dynamodb = new AWS.DynamoDB.DocumentClient()
const lambda = new AWS.Lambda()
// lambda function handler
exports.handler = async (event) => {
let result = await dynamodb.get({/* some get config */}).promise()
// do stuff ...
// kick off next lambda
await lambda.invoke({/* lambda config */}).promise()
return {"status": "ok"} // or something
}
Here is the unit test solution:
index.js:
const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-west-2' });
const dynamodb = new AWS.DynamoDB.DocumentClient();
const lambda = new AWS.Lambda();
exports.handler = async (event) => {
let result = await dynamodb.get({}).promise();
await lambda.invoke({}).promise();
return { status: 'ok' };
};
index.test.js:
const sinon = require('sinon');
const AWS = require('aws-sdk');
describe('61516053', () => {
afterEach(() => {
sinon.restore();
});
it('should pass', async () => {
const mLambda = { invoke: sinon.stub().returnsThis(), promise: sinon.stub() };
sinon.stub(AWS, 'Lambda').callsFake(() => mLambda);
const mDocumentClient = { get: sinon.stub().returnsThis(), promise: sinon.stub() };
sinon.stub(AWS.DynamoDB, 'DocumentClient').callsFake(() => mDocumentClient);
sinon.stub(AWS.config, 'update');
const { handler } = require('./');
await handler();
sinon.assert.calledWith(AWS.config.update, { region: 'us-west-2' });
sinon.assert.calledOnce(AWS.DynamoDB.DocumentClient);
sinon.assert.calledOnce(AWS.Lambda);
sinon.assert.calledWith(mLambda.invoke, {});
sinon.assert.calledOnce(mLambda.promise);
sinon.assert.calledWith(mDocumentClient.get, {});
sinon.assert.calledOnce(mDocumentClient.promise);
});
});
unit test results with 100% coverage:
61516053
✓ should pass (907ms)
1 passing (915ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------

Mocking dynamodb scan using jest

For the source code(dynamodb.js):
const AWS = require("aws-sdk");
const Promise = require("bluebird");
const client = new AWS.DynamoDB.DocumentClient();
module.exports.db = (method, params) => {
console.log("access dynamodb ");
return Promise.fromCallback(cb => client[method](params, cb));
};
Using the test that looks like this(dont want't to mock Promise.fromCallback):
describe("test", () => {
const realAWS = require("aws-sdk");
let fakePromise;
let fakeDynamo;
let dbClient;
beforeAll(function() {
fakePromise = jest.fn();
fakeDynamo = {
get: (params, cb) => {
fakePromise(params, cb);
}
};
realAWS.DynamoDB.DocumentClient = jest.fn(() => fakeDynamo);
dbClient = require("../dynamodb");
});
test.only("Test successed", done => {
let result = dbClient.db("get", null);
console.log("access dynamodb ");
expect(fakePromise).toHaveBeenCalled();
});
});
but when tun the test ,the error was heppend:
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.
if anyone can help me? Thanks!
You need to mock the implementation of the client.get method as a node callback style. So that Promise.fromCallback method will covert callback style to a promise.
Besides, you need to trigger the callback in your test case. That's why you got a timeout error.
E.g.
dynamodb.js:
const AWS = require('aws-sdk');
const Promise = require('bluebird');
const client = new AWS.DynamoDB.DocumentClient();
module.exports.db = (method, params) => {
console.log('access dynamodb');
return Promise.fromCallback((cb) => client[method](params, cb));
};
dynamodb.test.js:
const { db } = require('./dynamodb');
const AWS = require('aws-sdk');
jest.mock('aws-sdk', () => {
const mClient = { get: jest.fn() };
const mDynamoDB = {
DocumentClient: jest.fn(() => mClient),
};
return { DynamoDB: mDynamoDB };
});
const mockClient = new AWS.DynamoDB.DocumentClient();
describe('54360588', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should pass', async () => {
mockClient.get.mockImplementationOnce((err, callback) => {
callback(null, 'fake data');
});
const actual = await db('get', null);
expect(actual).toBe('fake data');
expect(mockClient.get).toBeCalledWith(null, expect.any(Function));
});
});
unit test result with coverage report:
PASS src/stackoverflow/54360588/dynamodb.test.js (10.74s)
54360588
✓ should pass (28ms)
console.log src/stackoverflow/54360588/dynamodb.js:177
access dynamodb
-------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
dynamodb.js | 100 | 100 | 100 | 100 | |
-------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 12.636s

nodejs - sinon stub not being called

For some reason I am having a little trouble getting this simple test to run correctly with a similar set-up I have used multiple times before.
Perhaps a fresh pair of eyes could help me understand why my method generateReport is not being invoked and none of my stubs are being triggered with the intended arguments?
BYE nor GOOD are ever logged and the tests are returning the error: AssertError: expected stub to be called with arguments
My index file:
const errorHandler = require('./lib/handlers/error-handler')
const transformRequest = require('./lib/handlers/request-converter')
const convert = require('./lib/convert')
exports.generateReport = function generateReport(req, res) {
console.log('HELLO')
const objectToPopulateTemplate = transformRequest(req.body)
convert(objectToPopulateTemplate, function (e, data) {
if (e) {
console.log('BYE')
const error = errorHandler(e)
return res.send(error.httpCode).json(error)
}
console.log('GOOD')
res
.set('Content-Type', 'application/pdf')
.set('Content-Disposition', `attachment; filename=velocity_report_${new Date()}.pdf`)
.set('Content-Length', data.length)
.status(200)
.end(data)
})
}
My test file:
const proxyquire = require('proxyquire')
const assert = require('assert')
const sinon = require('sinon')
const fakeData = require('./data/sample-request.json')
describe('report-exporter', () => {
describe('generateReport', () => {
const fakeError = new Error('Undefined is not a function')
let res, resSendStub, resStatusStub, resEndStub, resSetStub, resJsonStub, req, convertStub, generate
before(() => {
resSendStub = sinon.stub()
resJsonStub = sinon.stub()
resStatusStub = sinon.stub()
resEndStub = sinon.stub()
resSetStub = sinon.stub()
convertStub = sinon.stub()
res = {
send: function(errorCode) {
return resSendStub(errorCode)
},
json: function(object) {
return resJsonStub(object)
},
set: function(arg1, arg2) {
return resSetStub(arg1, arg2)
},
status: function(code) {
return resStatusStub(code)
},
end: function(data) {
return resEndStub(data)
}
}
req = {
body: {}
}
generate = proxyquire('./../index', {
'./lib/convert': function() {
return convertStub
}
})
})
it('Should return an error response', () => {
convertStub.throws(fakeError)
generate.generateReport(req, res)
sinon.assert.calledWith(resSendStub, '500')
})
})
})
It looks like you are proxyquireing your ./lib/convert incorrectly. Original convert is called with objectToPopulateTemplate and a callback function (e, data). And that's the callback who is responsible for error handling and sending a response.
The stubbed convert function though doesn't care about about the provided callback at all, so the callback never gets called.
Most likely what you want is to pass the parameters to convertStub and deal with them later:
'./lib/convert': function(objectToPopulateTemplate, cb) {
return convertStub(objectToPopulateTemplate, cb);
}
and then
it('Should return an error response', () => {
generate.generateReport(req, res);
const cb = convertStub.getCall(0).args[1];
// simulate `convert` to fail
cb(fakeError);
sinon.assert.calledWith(resSendStub, '500')
})
Here is the unit test solution:
index.js:
const errorHandler = require("./error-handler");
const transformRequest = require("./request-converter");
const convert = require("./convert");
exports.generateReport = function generateReport(req, res) {
console.log("HELLO");
const objectToPopulateTemplate = transformRequest(req.body);
convert(objectToPopulateTemplate, function(e, data) {
if (e) {
console.log("BYE");
const error = errorHandler(e);
return res.send(error.httpCode).json(error);
}
console.log("GOOD");
res
.set("Content-Type", "application/pdf")
.set(
"Content-Disposition",
`attachment; filename=velocity_report_${new Date()}.pdf`
)
.set("Content-Length", data.length)
.status(200)
.end(data);
});
};
error-handler.js:
module.exports = function(error) {
return error;
};
request-converter.js:
module.exports = function transformRequest(body) {
return body;
};
convert.js:
module.exports = function convert(body, callback) {
callback();
};
index.spec.js:
const sinon = require("sinon");
const proxyquire = require("proxyquire");
describe("report-exporter", () => {
describe("generateReport", () => {
afterEach(() => {
sinon.restore();
});
const fakeError = new Error("Undefined is not a function");
fakeError.httpCode = 500;
it("Should return an error response", () => {
const logSpy = sinon.spy(console, "log");
const mReq = { body: {} };
const mRes = { send: sinon.stub().returnsThis(), json: sinon.stub() };
const convertStub = sinon.stub();
const errorHandlerStub = sinon.stub().returns(fakeError);
const transformRequestStub = sinon.stub().returns(mReq.body);
const generate = proxyquire("./", {
"./convert": convertStub,
"./error-handler": errorHandlerStub,
"./request-converter": transformRequestStub
});
generate.generateReport(mReq, mRes);
convertStub.yield(fakeError, null);
sinon.assert.calledWith(transformRequestStub);
sinon.assert.calledWith(convertStub, {}, sinon.match.func);
sinon.assert.calledWith(errorHandlerStub, fakeError);
sinon.assert.calledWith(logSpy.firstCall, "HELLO");
sinon.assert.calledWith(logSpy.secondCall, "BYE");
});
});
});
Unit test result with coverage report:
report-exporter
generateReport
HELLO
BYE
✓ Should return an error response (94ms)
1 passing (98ms)
----------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------------------|----------|----------|----------|----------|-------------------|
All files | 87.5 | 50 | 62.5 | 87.5 | |
convert.js | 50 | 100 | 0 | 50 | 2 |
error-handler.js | 50 | 100 | 0 | 50 | 2 |
index.js | 84.62 | 50 | 100 | 84.62 | 14,15 |
index.spec.js | 100 | 100 | 100 | 100 | |
request-converter.js | 50 | 100 | 0 | 50 | 2 |
----------------------|----------|----------|----------|----------|-------------------|
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/47352972

Resources