Backbone.js Tutorial: Testing with Mocks

14 Feb 2013 | By Alex Young | Tags backbone.js mvc node backgoog testing

Preparation

Before starting this tutorial, you’ll need the following:

  • alexyoung / dailyjs-backbone-tutorial at commit 5b0a529
  • The API key from part 2
  • The “Client ID” key from part 2
  • Update app/js/config.js with your keys (if you’ve checked out my source)

To check out the source, run the following commands (or use a suitable Git GUI tool):

git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git
cd dailyjs-backbone-tutorial
git reset --hard 5b0a529

Mocks

Last week I wrote about testing a custom Backbone.sync implementation using Sinon’s spies. This worked well in our situation where the transport layer isn’t necessarily pinned down – Sinon includes Fake XMLHttpRequest, but this won’t work with Google’s API as far as I know. This week I want to introduce another testing concept that Sinon provides: mocks.

Mocks are fake methods that allow expectations to be registered. Historically, you’ll find mocks being used in unit tests where I/O occurs. If you’re testing business logic you don’t need to check if a file was written or a network call was made, it’s often preferable to attach an expectation to make sure the appropriate API would have been called.

In Sinon, creating a mock returns an object that can be decorated with expectations. The API is chainable, so it’s low on boilerplate and high on readability. What you’re aiming to do is state “whenever this method is called, ensure it was called with these parameters”. This can be done through mocks by setting up expectations using matchers.

Matchers are similar to assertions – they can be used to check that arguments are everything from primitive types to instances of a constructor, or even literal values.

Last week we used spies to ensure Google’s API was accessed in the expected way. Mocks could be used for this as well. We don’t really care about the request so much as the fact a particular CRUD operation was requested. The signature for Backbone.gapiRequest is request, method, model, options – the method argument is generally what we’re interested in. Therefore, to set up an expectation that saving an existing task caused update to fire, we can use a mock with sinon.match.object:

var mock = sinon.mock(Backbone);
mock.expects('gapiRequest').once().withArgs(sinon.match.object, 'update');

// Do UI stuff to cause the task to be edited and the form to be submitted
mock.verify();

Mocks Compared to Spies

The previous example looked a lot like last week’s spies, and using spies for the same thing used less code. So, when should we use mocks, and when should we use spies? Mocks give you a fine-grained control over the order and behaviour of method calls. Spies have a different API which focuses on checking how callbacks or methods are used. If you were testing a method that accepts a callback, you could pass in a spy to see how the callback gets used. With a mock, the callback would be from the system under test, and you’d set up expectations on it.

When it comes to UI testing – triggering interface actions to invoke code, I find it’s easier to treat the entire Backbone stack as a whole and use spies to ensure the expected behaviour occurs. Rather than writing a test for each model, view, and collection, it makes more sense to drive the UI and hook into model or sync operations to verify the outcome.

In last week’s tests where lists were being tested, I probably wouldn’t use mocks because mocks should have a closer relationship to a given method under test. The kinds of tests we’re writing involve more than one method, so spies and assertions on the DOM make more sense.

Mock Example

A good place to use mocks is for testing app/js/gapi.js. Let’s say we’re interested in making sure gapiRequest gets called by Backbone.sync. We could use mocks:

test('gapiRequest is called by Backbone.sync', function() {
  var mock = sinon.mock(Backbone);
  mock.expects('gapiRequest').once();
  Backbone.sync('update', model, {});
  mock.verify();
});

This calls Backbone.sync to cause gapiRequest to be called once. This test doesn’t verify the behaviour of gapiRequest itself, just the fact it gets called.

One quirk of the custom Backbone.sync API is Task.prototype.get is called twice: once to fetch task’s ID, and another to get the list’s ID. We could test this with mocks if it was deemed important:

test('Ensure Task.prototype.get is called twice', function() {
  var mock = sinon.mock(model);

  mock.expects('get').twice().returns(model.id);
  Backbone.sync('update', model);
  mock.verify();
});

This uses the twice expectation with another mock.

Hopefully you’re starting to understand how mocks and spies differ. There’s another major part of Sinon, though, and that’s the stub API.

Stubs

Digging further into Backbone.gapiRequest, requests are expected to have an execute method which gets called to send data to Google’s API. Both spies and stubs can be used to test this using the yieldsTo method:

test('gapiRequest causes the execute callback to fire', function() {
  var spy = sinon.spy();
  sinon.stub(Backbone, 'gapiRequest').yieldsTo('execute', spy);
  Backbone.sync('update', model);

  assert.ok(spy.calledOnce);
  Backbone.gapiRequest.restore();
});

This test causes the following chain of events to occur:

  1. Backbone.sync calls Backbone.gapiRequest
  2. Backbone.gapiRequest receives an object with an execute property, which we’ve replaced with a spy
  3. Backbone.gapiRequest calls this execute method, therefore satisfying assert.ok(spy.calledOnce)

Putting these ideas together can be used to make sure the right success or error callbacks are triggered after a request has completed:

test('Errors get called', function() {
  var spy = sinon.spy()
    , options = { error: spy }
    ;

  // Stub the internal update method that would usually come from Google
  sinon.stub(gapi.client.tasks.tasks, 'update').returns({
    execute: sinon.stub().yields(options)
  });

  // Invoke a sync with a fake model and the options with the error callback
  Backbone.sync('update', model, options);

  assert.ok(spy.calledOnce);
  gapi.client.tasks.tasks.update.restore();
});

This test makes sure error gets called by using a spy, and it also stubs out gapi.client.tasks.tasks.update with our own object. This object has an execute property which causes the callback inside gapiRequest to run, and ultimately call error.

Clearing Up

I’ve written a test suite for tasks. It’s based on last week’s tests so there isn’t really anything new, apart from the teardown method:

setup(function() {
  /// ...

  spyUpdate = sinon.spy(gapi.client.tasks.tasks, 'update')
  spyCreate = sinon.spy(gapi.client.tasks.tasks, 'insert')
  spyDelete = sinon.spy(gapi.client.tasks.tasks, 'delete')

  // ...
});

teardown(function() {
  gapi.client.tasks.tasks.update.restore();
  gapi.client.tasks.tasks.insert.restore();
  gapi.client.tasks.tasks.delete.restore();
});

I’ve found this pattern is better than calling reset, because it’s easy to attempt to wrap objects more than once when multiple test files are loaded.

Writing Good Tests with Sinon

Sinon might look like a small library that you can drop into Mocha, Jasmine, or QUnit, but there’s an art to writing good Sinon tests. Sinon’s documentation has some explanation of when exactly spies, mocks, and stubs are useful, but there is a subjective factor at play particularly when it comes to deciding whether a test is best written with a mock or a stub.

A few tips I’ve found useful are:

  • Spies are great for the times when you want to test the entire application, rather than a specific class or method
  • Stubs come in handy when there are methods you don’t want run or want to force execution down a given path
  • Mocks are good for testing specific methods
  • A single mock per test case should be used
  • You should use restore() after using spies and stubs, it’s easy to forget and causes “double wrap” errors

Summary

Stylistically spies, stubs, and mocks are very different, but they’re vexingly similar until you’ve had some practice with Sinon. There have been mock vs. stub discussions on the Sinon.JS Google Group, so it’s probably best to ask Christian on that group if you’re struggling get Sinon to do what you want.

The source for this tutorial can be found in alexyoung / dailyjs-backbone-tutorial, commit 0c6de32.


blog comments powered by Disqus