How to mock chained functions?

Mocking results of a single function is crystal clear in most use cases. However, there is a level of difficulty linked to mocking more than one chained function. This article highlights some techniques to overcome this challenge.

In this article we will talk about:

Even though this blogpost was designed to offer complementary materials to those who bought my Testing nodejs Applications book, the content can help any software developer to tuneup working environment. You use this link to buy the book. Testing nodejs Applications Book Cover

Show me the code

//standalone
module.exports = function normalizeName(obj){
  let attrs = (obj.name || "").split(' ');
  return {
    first: attrs[0], 
    last: attrs[attrs.length - 1], 
    full: obj.name
  };
};

//with callback
Order
  .find()
	.populate()
	.sort()
	.exec(function(err, order){ 
    /** ... */
  });

//with a promise
Order
  .find()
  .populate()
  .sort()
  .exec()
  .then(function(err, order){ });

Keyvan Fatehi managed to hack something amazing ~ that is in fact the blueprint of this article

What can possibly go wrong?

If you haven't already, there is an article about “Test Doubles” that makes a case on how stub/mock and spy stacks-up. There is also “How to spy/stub methods” as a complementary article.

Some challenges we notice by looking at the above examples:

Show me the tests

Functions' return value mocks can be achieved via two main channels: via a spy of a function, or via a stub on an object that hosts the function.

The choice to use mongoose models is deliberate, for having something that is widely used, at least for backend developers. However, we have to highlight techniques presented below, can be applied to any other object or function. There are also some advanced techniques such as asynchronous code via callbacks, promises, and even streaming backed-in the library, that would otherwise increase complexity to examples that we want to be simple.

Standalone Functions

Before we jump headfirst into it, let's see what infrastructure sinon has to offer when it comes to mocking(spying and stubbing included).


let outputMock = { ... };
sinon.stub(obj, 'func').returns(outputMock);
sinon.stub(obj, 'func').callsFake(function fake(){ return outputMock; })
let func = sinon.spy(function fake(){ return outputMock; });

With exception of spy notation, backing objects are required in the previous three examples to be able to mock function return values. This is telling, to mock an independent function, we will either have to re-assign function to a spy whenever the function is needed, or attach our independent function to some sort/form of an object.

Using modularization techniques + index we can solve this challenge. If normalizeName() is located in /utils directory, in /utils/index.js we will export all files under /utils. We will then be able to mock utils#normalizeName using one of the examples stated earlier.

//ES5
var normalizeName = require('./normalize-name');
module.exports = { normalizeName: noramlizeName };
//ESNext
export * from "../utils";

//ES5
var utils = require('./utils');
//ESNext
import * as utils from './utils';

//Mocking
let nameMock = {first: 'Eliud', last: 'Kipchoge', full: 'Eliud Kipchoge'};
sinon.stub(utils, 'normalizeName').returns(nameMock);
sinon.stub(utils, 'normalizeName').callsFake(() => nameMock)
let normalizeName = sinon.spy(() => nameMock);

The use of the arrow function is to have a small example, but can easily be replaced with full-fledged function declarations.

Chained Functions

Stubbing chained functions. One of the tricks to make a function chain-able is to return a special kind of object. This object has to have access to both previous and new function contexts.

module.exports = utils = {
  name: '', 
  _name: {full: this.name},
  first: function(optional){
    if(optional) { this.name = optional; }
    this._name.full = this.name; 
    this._name.first=  this.name.split(' ')[0];
    return this;
  },
  last: function(optional){
    if(optional) { this.name = optional; }
    this._name.full = this.name; 
    let attrs = this.name.split(' ');
    this._name.last = attrs[attrs.length - 1];
    return this;
  }, 
  value: function(){ 
    return this._name; 
  }
};

console.log(utils.first('Eliud Kipchoge').last().value());
//logs { first: "Eliud", last: "Kipchoge", full: "Eliud Kipchoge"}

From this angle, we can choose to mock the last function in the call tree and let other functions do their job, or mock in a cascading fashion.

//Mock the last function call
let nameMock = {first: 'Eliud', last: 'Kipchoge', full: 'Eliud Kipchoge'};
sinon.stub(utils, 'normalizeName').returns(nameMock);

//Mock in a cascading fashion
sinon.stub(util, 'first').returns({
  last: sinon.stub().returns({
    value: sinon.stub().returns(nameMock)
  })
})

Example:

The depth of cascading stub/mocks depends on how far function usage is headed. The more chaining, the deeper the stubbing can get.

Chained Methods

Stubbing chained methods are not different from stubbing chained functions. Methods are by definition a collection of functions belonging to the same class. Since objects are instances of a class, we can keep the mental model we adopted earlier intact.

Order.find()
	.populate()
	.sort()
	.exec(function(err, order){ /** ... */});

//Slight modification of original code
sinon.stub(Order, 'find').returns({
  populate: sinon.stub().returns({
    exec: sinon.stub().yields(null, {
      id: "1234553"
    })
  })
})

Chained Methods with a promise

What can happen if a promise is involved in a chain of functions?

We have two alternatives and both are important just equally. We can opt to adopt the cascading approach we used in previous examples, Or adopt a library that does some heavy lifting for us. The set of libraries, to be more specific, sinon-mongoose and sinon-as-promised to have sinon like mocking experience with capabilities to resolve promises.


require('sinon');
require('sinon-as-promised');
require('sinon-mongoose');

sinon.mock(Order)
  .expects('find')
  .populate('customer provider product')
  .chain('limit').withArgs(10)
  .chain('sort').withArgs('-date')
  .chain('exec')
  .resolves(resultsMock)

Conclusion

In this article, we revisited strategies to mock chained functions which are supposed to return data on each point of connection. We also re-iterated the difference between stubbing and mocking, and how spies(fake) fall into the testing picture. There are additional complimentary materials in the “Testing nodejs applications” book.

References