If you’re unit testing Javascript use Sinon.js, it’s more useful than you expect

For a long time I didn’t use any unit testing libraries with Javascript. After all unlike Java you can do anything with your objects. If you want your car to be a cat that can walk you simply modify the object directly. So what’s the point of having a mocking library?

I discovered sinon.js a few months ago and immediately fell in love. It made me realize how much useless boilerplate code I had in my unit tests and immediately helped me write cleaner, more elegant code.

Here’s a very basic example of how I used to mock functions before and after sinon:

/** Before sinon.js **/

getAnimationSheetArgs = null;
impactMock.game.cache.getAnimationSheet = function() {
getAnimationSheetArgs = arguments
};
bullet.loadAnimations();
assert(getAnimationSheetArgs != null);
assert.equal(getAnimationSheetArgs[0], "img/bullets/awesome.png");
assert.equal(getAnimationSheetArgs[1], 5);
assert.equal(getAnimationSheetArgs[2], 15);
/** After sinon.js **/

impactMock.game.cache.getAnimationSheet = sinon.spy();
bullet.loadAnimations();
assert(impactMock.game.cache.getAnimationSheet.calledWith("img/bullets/awesome.png", 5, 15));

Before Sinon I had to declare a variable to hold the arguments passed to each function I wanted to mock. After using sinon this became one line and verifying the correct arguments were passed to the function can be done in one function call.

It may not look like much, but when you have over 1000 unit tests (as Tower Storm now has) it adds up to a lot of time saved.

Sinon also provides tons of functionality to stub out functions. You can make a method automatically return certain values or even call a callback with specific arguments (for testing async code).

Let’s say you want to test a render function to ensure user details are displayed. It looks like this:

var AdminController = {
userInfo: function (req, res) {
userId = req.param('id');
User.findById(userId, function (err, user) {
if (err) return res.send(500);
res.jsonp(200, user.data);
});
}
}

Now we only want to test that user.data is being sent to the browser. We don’t want to actually hit the database and find a user with findById so we need to mock it out.

it("Should send user.data to the browser", function () {
var mockUser = {data: {name: 'test'}};
sinon.stub(User, 'findById').callsArgWith(1, null, mockUser);
req = {param: sinon.stub().returns(123)};
res = {jsonp: sinon.spy()};
AdminController.userInfo(req, res);
assert(res.jsonp.calledWith(200, {name: 'test'}));
User.findById.restore()
});

On line 1 we create a mock user which we want to display. Then we stub the User.findById method to instantly call argument 1 (the callback) with the 2 arguments null and mockUser (for it’s err and user arguments).
On line 3 we create req as an object with just the param method. We set param to a sinon stub and make it instantly return 123 (the user’s id, although it can return anything as User.findById doesn’t even use it).
On line 4 we create res as an object that only has a jsonp method. We set this method to be a sinon spy as it doesn’t need to return anything, it only needs to record what it was called with.
On lines 5 and 6 we call the method and check that res.jsonp was successfully called with the users data using sinons handy calledWith function.
Finally on line 7 we call restore on User.findById to remove the stub and restore it’s original functionality. This is so if we have tests in the future that want to use the original function they won’t break unexpectedly.

This is by far the easiest way I’ve found to mock and unit test javascript though if you know of a better way let me know. I’m always trying to be as efficient as possible.

 

How to mock and unit test moment.js

I’ve been using moment.js in some client work for a rather complex booking form.Unfortunately date related functionality is almost impossible to manually test so we implemented unit tests to ensure everything worked as it should. Code like this is perfect for unit testing because it’s completely rules based.

Mocking moment.js seemed hard at first but wasn’t too complicated after a bit of thought. Here’s an example of how to mock it using mocha.

First we have the booking manager validate function that needs to be tested. This function ensures that customers cannot make bookings for the next day after 3pm. It also ensures customers cannot make bookings for saturday and monday after 3pm on friday.

/** booking-manager.js **/

BookingManager = {
getCurrentTime: function () {
return moment().tz(config.TIMEZONE)    
},
validate: function(formData) {
var bookingDay, currentTime;
currentTime = this.getCurrentTime();
bookingDay = moment(formData.dateAndTime.date, "YYYYMMDD").tz(config.TIMEZONE);
if (bookingDay.isBefore(currentTime, 'day') || bookingDay.isSame(currentTime, 'day')) {
return false;
}
if (currentTime.format('HH') >= config.NEXT_DAY_CUTOFF_TIME) {
if (bookingDay.isSame(moment(currentTime).add('days', 1), 'day')) {
return false;
}
if (currentTime.day() === 5 && bookingDay.day() === 1) {
return false;
}
}
if ((currentTime.day() === 6 || currentTime.day() === 0) && bookingDay.day() === 1) {
return false;
}
return true;
}
}

Now we need to test this function to ensure it works. The problem is we have to test booking restrictions are working on every day of the week. So we need to mock out the getCurrentTime function to pretend we’re submitting the form at different times.

/* booking-manager-unit-test.js **/

describe("BookingManager", function() {
var currentDayOfWeek, currentHour, originalGetCurrentTime, formData;
currentHour = null;
currentDayOfWeek = null;
originalGetCurrentTime = null;
formData = null;

beforeEach(function() {
var currentDayOfWeek, currentHour, originalGetCurrentTime;
currentHour = 12;
currentDayOfWeek = 1;
formData = { dateAndTime: { date: null }};
originalGetCurrentTime = bookingManager.getCurrentTime;
bookingManager.getCurrentTime = function() {
var currentTime;
currentTime = originalGetCurrentTime.call(bookingManager);
currentTime.day(currentDayOfWeek);
currentTime.hour(currentHour);
return currentTime;
};
});

afterEach(function() {
return bookingManager.getCurrentTime = originalGetCurrentTime;
});
});



This is the meat of the mocking. We set up two variables currentHour and currentDayOfWeek. Then we use these in our mock getCurrentTime function to fake the current time.

describe("validate", function() {
it("Should return false if the booking date is the current day or before", function() {
var formData, today;
today = bookingManager.getCurrentTime();
formData.dateAndTime.date = today.format('YYYYMMDD');
assert.equal(bookingManager.validate(formData), false);
});

it("Should return false if the booking is a next day booking and the time is past 3pm", function() {
var currentHour, formData, tomorrow;
currentHour = 16;
tomorrow = bookingManager.getCurrentTime().add('days', 1);
formData.dateAndTime.date = tomorrow.format('YYYYMMDD');
assert.equal(bookingManager.validate(formData), false);
});

it("Should return false if the booking is a next day booking and the time hour is 3pm", function() {
var currentHour, formData, tomorrow;
currentHour = 15;
tomorrow = bookingManager.getCurrentTime().add('days', 1);
formData.dateAndTime.date = tomorrow.format('YYYYMMDD');
assert.equal(bookingManager.validate(formData), false);
});

it("Should return false if the booking is made on saturday for monday", function() {
var currentDayOfWeek, formData, monday;
currentDayOfWeek = 6;
monday = bookingManager.getCurrentTime().add('days', 2);
formData.dateAndTime.date = monday.format('YYYYMMDD');
assert.equal(bookingManager.validate(formData), false);
});

it("Should return false if the booking is made on sunday for monday", function() {
var currentDayOfWeek, formData, monday;
currentDayOfWeek = 0;
monday = bookingManager.getCurrentTime().add('days', 1);
formData.dateAndTime.date = monday.format('YYYYMMDD');
assert.equal(bookingManager.validate(formData), false);
});

it("Should return false if the booking is made on friday after 3pm for monday", function() {
var currentDayOfWeek, currentHour, formData, monday;
currentDayOfWeek = 5;
currentHour = 15;
monday = bookingManager.getCurrentTime().add('days', 3);
formData.dateAndTime.date = monday.format('YYYYMMDD');
assert.equal(bookingManager.validate(formData), false);
});

it("Should return true if the booking is made on friday before 3pm for monday", function() {
var currentDayOfWeek, currentHour, formData, monday;
currentDayOfWeek = 5;
currentHour = 13;
monday = bookingManager.getCurrentTime().add('days', 3);
formData.dateAndTime.date = monday.format('YYYYMMDD');
assert.equal(bookingManager.validate(formData), true);
});

it("Should return true if the booking is a next day booking and the time is before 3pm", function() {
var currentHour, formData, tomorrow;
currentHour = 12;
tomorrow = bookingManager.getCurrentTime().add('days', 1);
formData.dateAndTime.date = tomorrow.format('YYYYMMDD');
assert.equal(bookingManager.validate(formData), true);
});

it("Should return true if it's past 3pm but the booking is not a next day booking", function() {
var currentHour, formData, future;
currentHour = 16;
future = bookingManager.getCurrentTime().add('days', 2);
formData.dateAndTime.date = future.format('YYYYMMDD');
assert.equal(bookingManager.validate(formData), true);
});
});


As you can see using this mock function is quite easy. You simply change the currentHour or currentDayOfWeek variables and that’s what the time becomes.

Have fun coding and let me know if you find any issues with the above code or feel it can be improved.