A spy is a function that records information about every call made to it. For example, instead of assigning an empty function to next, we can assign a spy to it. Whenever next is invoked, information about each invocation is stored inside the spy object. We can then use this information to determine the number of times the spy has been called.
The de facto spy library in the ecosystem is Sinon (sinonjs.org), so let's install it:
$ yarn add sinon --dev
Then, in our unit test, import the spy named export from the sinon package:
import { spy } from 'sinon';
Now, in our test function, instead of assigning an empty function to next, assign it a new spy:
const next = spy();
When the spy function is called, the spy will update some of its properties to reflect the state of the spy. For example, when it's been called once, the spy's calledOnce property will be set to true; if the spy function is invoked again, the calledOnce property will be set to false and the calledTwice property will be set to true. There are many other useful properties such as calledWith, but let's update our it block by checking the calledOnce property of our spy:
it('should call next() once', function () {
assert(next.calledOnce);
});
Next, we'll define more tests to examine what happens when req.method is one of POST, PATCH, or PUT. Implement the following tests, which test what happens when the content-length header is not 0:
describe('checkEmptyPayload', function () {
let req;
let res;
let next;
...
(['POST', 'PATCH', 'PUT']).forEach((method) => {
describe(`When req.method is ${method}`, function () {
describe('and the content-length header is not "0"', function () {
let clonedRes;
beforeEach(function () {
req = {
method,
headers: {
'content-length': '1',
},
};
res = {};
next = spy();
clonedRes = deepClone(res);
checkEmptyPayload(req, res, next);
});
it('should not modify res', function () {
assert(deepEqual(res, clonedRes));
});
it('should call next()', function () {
assert(next.calledOnce);
});
});
});
});
});
beforeEach is another function that is injected into the global scope by Mocha. beforeEach will run the function passed into it, prior to running each it block that resides on the same or lower level as the beforeEach block. Here, we are using it to invoke checkEmptyPayload before each assertion.
Next, when the content-type header is 0, we want to assert that the res.status, res.set, and res.json methods are called correctly:
describe('and the content-length header is "0"', function () {
let resJsonReturnValue;
beforeEach(function () {
req = {
method,
headers: {
'content-length': '0',
},
};
resJsonReturnValue = {};
res = {
status: spy(),
set: spy(),
json: spy(),
};
next = spy();
checkEmptyPayload(req, res, next);
});
describe('should call res.status()', function () {
it('once', function () {
assert(res.status.calledOnce);
});
it('with the argument 400', function () {
assert(res.status.calledWithExactly(400));
});
});
describe('should call res.set()', function () {
it('once', function () {
assert(res.set.calledOnce);
});
it('with the arguments "Content-Type" and "application/json"', function () {
assert(res.set.calledWithExactly('Content-Type', 'application/json'));
});
});
describe('should call res.json()', function () {
it('once', function () {
assert(res.json.calledOnce);
});
it('with the correct error object', function () {
assert(res.json.calledWithExactly({ message: 'Payload should not be empty' }));
});
});
it('should not call next()', function () {
assert(next.notCalled);
});
});
Lastly, we need to test that checkEmptyPayload will return the output of res.json(). To do that, we need to use another test construct called stubs.