Mocking methods of a JavaScript object created within a function

Tags: , , , ,



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?

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


Source: stackoverflow