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:
- Stubbing standalone functions
- Stubbing chained functions
- Stubbing chained methods
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.
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:
- Given that most spying/stubbing techniques require an object, which standalone functions do not have
- Each chaining results in an independent object, which makes spying/stubbing methods a little off the charts
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
- Testing
nodejs
Applications book - Mocking/Stubbing/Spying
mongoose
models ~ CodeUtopia Blog - Working response on stubbing StackOverflow
- Mocking Model Level ~ SemaphoreCI Community Tutorials