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 | --------------|---------|----------|---------|---------|-------------------