I’ve written a JavaScript function that creates an object from a require()’d library, and then uses it. That seems to be causing me trouble when I try to write tests for it because I don’t seem to have a good way to gain control over that object and create mocks of its methods to test the behavior of my function.
Am I running into this because I’ve designed the function poorly? I come from a Java/Spring background, so the voices in my head are screaming “dependency injection”. Is there’s a better way to do that than just passing the object my function needs into it as a parameter?
Example function:
// dbService.js
const AWS = require('aws-sdk');
function getItem() {
const dynamo = new AWS.DynamoDB.DocumentClient();
var params = {/* irrelevant */}
try {
return await dynamo.get(getParams).promise();
} catch (err) {
return err;
}
}
exports.getItem = getItem;
I start running into jams when I try to write tests to verify my function’s behavior when dynamo.get()
returns successfully or throws an error.
Example test (I’ve been using Sinon for mocking and Chai for asserting):
// dbServiceTest.js
const sinon = require('sinon');
const dbService = require('dbService.js');
const expect = require('chai').expect;
describe('dbService: When database returns a record', function() {
let dbMock, dbServiceResp = null;
beforeEach(async function() {
dbMock = sinon.stub(dynamo, "get")
.returns({Item: "an item"});
dbServiceResp = await dbService.getItem("an item");
});
afterEach(function() {
dbMock.restore();
});
it('Should have expected value', function() {
expect(dbServiceResp.Item).to.be.equal("an item");
});
});
It seems obvious that the mock of dynamo.get()
I’ve created doesn’t get used by dbService.getItem()
because dbService.getItem()
completely owns the instantiation of its own dependency on a DocumentClient
object.
Should I just pass a DocumentClient
into my getItem()
function, or is there a better way?
Advertisement
Answer
DI is the best way, it will make your code easier to test, better scalability, and decouple the modules. But you still can stub the aws-sdk
module if you want to require
the module as the dependency. Unit test solution:
dbService.js
:
const AWS = require('aws-sdk');
async function getItem() {
const dynamo = new AWS.DynamoDB.DocumentClient();
var params = {
/* irrelevant */
};
try {
return await dynamo.get(params).promise();
} catch (err) {
return err;
}
}
exports.getItem = getItem;
dbService.test.js
:
const sinon = require('sinon');
const AWS = require('aws-sdk');
const expect = require('chai').expect;
describe('dbService: When database returns a record', function() {
afterEach(() => {
sinon.restore();
});
it('Should have expected value', async function() {
const mDynamo = { get: sinon.stub().returnsThis(), promise: sinon.stub().resolves({ Item: 'an item' }) };
const mDocumentClient = sinon.stub(AWS.DynamoDB, 'DocumentClient').returns(mDynamo);
const dbService = require('./dbService');
const actual = await dbService.getItem();
expect(actual.Item).to.be.equal('an item');
sinon.assert.calledOnce(mDocumentClient);
sinon.assert.calledWithExactly(mDynamo.get, {});
sinon.assert.calledOnce(mDynamo.promise);
});
it('should return error', async () => {
const mError = new Error('network');
const mDynamo = { get: sinon.stub().returnsThis(), promise: sinon.stub().rejects(mError) };
const mDocumentClient = sinon.stub(AWS.DynamoDB, 'DocumentClient').returns(mDynamo);
const dbService = require('./dbService');
const actual = await dbService.getItem();
expect(actual.message).to.be.eql('network');
sinon.assert.calledOnce(mDocumentClient);
sinon.assert.calledWithExactly(mDynamo.get, {});
sinon.assert.calledOnce(mDynamo.promise);
});
});
unit test result:
dbService: When database returns a record
✓ Should have expected value
✓ should return error
2 passing (26ms)
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
dbService.js | 100 | 100 | 100 | 100 |
--------------|---------|----------|---------|---------|-------------------