Skip to content

cjohansen.no

JavaScript Test Spies, Stubs and Mocks

One of the things coming out of me recently writing a book (apart from, you know, a book) is a JavaScript stubbing and mocking library (still in the making) called Sinon. This is the first of a total of three posts giving a preview on the API. Hopefully some of you want to share your ideas and feedback on it.

In this post we'll walk through the core of the library; spies, stubs and mocks. I'll cover the basic concept of the library and show most of the related API.

Goals of Sinon

So, why write another stubbing and mocking library? There are already a few out there, and several testing libraries ship with built-in stubs and mocks. I originally wrote Sinon for the book, with the intention of using it for one of the "TDD by example" chapters. That didn't happen, but I still used it as an example of using a library for stubbing and mocking (as opposed to manual stubbing, which works for most simple use cases).

I had some design requirements for such a library:

Sinon passes these requirements. It's standalone, it lives entirely within the sinon object, has a (hopefully) simple API and provides lots of tools to reduce the required amount of scaffolding. This post focuses on the API, and I'll post a follow-up soon covering the integration part.

Status

Currently, the Sinon core, covered in this post, is functional but the library as a whole is not feature complete, neither is the API frozen. By sharing info about it at this point I hope to lower the bar for providing feedback and suggestions. The main shortcoming at this point, however is documentation. I will not do a "release" until I have proper documentation. This post, and the two following it is a start.

Enough chatter, on to the code.

Test spies

A test spy is an object that records its interaction with other objects throughout the code base. When deciding if a test was successful based on the state of available objects alone is not sufficient, we can use test spies and make assertions on things such as the number of calls, arguments passed to specific functions, return values and more.

Sinon implements spies as functions that wrap other functions and records each and every call, including the value of this, arguments, return value and any exceptions thrown. It is possible to spy on live methods without interfering with their normal behavior, although in practice you will rarely create a spy directly. Both stubs and mocks implement the spy interface.

Spies have a few properties, and a handful of methods. This is how you'd use a spy on jQuery's ajax method:

sinon.spy(jQuery, "ajax");
jQuery.getJSON("/some/data");

Doing this does not interfere with normal operation of jQuery.ajax, but it wraps the method and records data.

Properties

The following are properties exposed by spies, and their corresponding values for the above example.

Call data

Spies store data about calls in four arrays. If you prefer to keep things low-level, these are all you need.

Methods

If you prefer to keep things on a low level, the four above arrays will provide you with everything you need to know about a spy's travel through a certain code path. If, however, you're more like me, and like high-level method calls that help tests clearly state their intent, you might be interested in the methods provided by the test spies.

In addition to these methods, spies have a special method to "unwrap" the method they are spying on:

jQuery.ajax.restore();

This unwraps and restores the original method. To avoid spies leaking from test to test, all spies can be restored in e.g. a test case's tearDown.

Using these methods with assertions can provide pretty good readability in tests:

"test should register click events for buttons": function () {
  sinon.spy(dom, "addEventHandler");
  var buttons = dom.get("div.button", this.form);

  this.feedback.setForm(this.form);

  assert(dom.addEventHandler.calledWith(buttons[0], "click"));
  assert(dom.addEventHandler.calledWith(buttons[1], "click"));
  assert(dom.addEventHandler.calledWith(buttons[2], "click"));
}

This test uses some homegrown tools in place of a library. It verifies that all buttons inside a form has click event handlers added to them. Note that the assertions only check the two first arguments - the element and event name, not the handler (which is covered by a separate test). Reviewing this example I realize I should've used event delegation in this case, but oh well...

Sinon has some even more high-level tools for working with spies, but I'll cover them in a later post when I show you Sinon's offerings for test library integration.

Stubs

Test stubs are fake objects with pre-programmed behavior. They are typically used for one of two reasons:

  1. To avoid some inconvenient interface - for instance to avoid making actual requests to a server from tests.
  2. To feed the system with known data, forcing a specific code path.

JavaScript has first class functions, which will take you a long way. Consider the following pseudo-code example:

"test should call event handler": function () {
  var handler = function () { handler.called = true; };
  var myElement = document.getElementsByTagName("a")[0];

  dom.addEventListener(myElement, "mouseover", handler);
  dom.fireEvent(myElement, "mouseover");

  assertTrue(handler.called);
}

Because JavaScript functions are so versatile, most simple uses for stubs are easy to hand roll. However, after doing that for a while you realize that using a library can possibly reduce manual scaffolding, especially when stubbing global interfaces, such as the previous example of jQuery.ajax.

Creating stubs

Sinon provides a simple API to build stubs. To create a stub, simply call sinon.stub(). It returns an "empty" function which in addition to support methods to instruct its behavior supports the aforementioned spy API. Thus, the above example could be solved with Sinon as follows:

"test should call event handler": function () {
  var handler = sinon.stub();
  var myElement = document.getElementsByTagName("a")[0];

  dom.addEventListener(myElement, "mouseover", handler);
  dom.fireEvent(myElement, "mouseover");

  assertTrue(handler.called);
}

Only slightly easier, but this is one of the easiest cases to support. Because it supports the entire spy interface we could have also checked the number of counts, this, arguments and more.

Stubs methods

The last two methods can prove very helpful once I get them right. I'd also like for the stub to be able to call callbacks passed as part of an "options" object. I haven't figured out a reasonably short and clear signature for such a method yet, though, so it's currently lacking. Suggestions, as always, are very welcome.

All the methods return the stub, so they can be chained, like so:

sinon.stub(jQuery, "each").callsArgWith(1, {}).returns({});

This stubs the jQuery.each method. When called, it will call its second argument with an empty object and then returns an empty object.

The real power in stubs is provided by the spy interface discussed previously. Remember that stubbed methods inherit the restore method as well, to restore the original method.

Mocks

Mock objects are like both stubs and spies, and additionally have pre-programmed behavior verification - so-called expectations - built in. Mocks state their success criteria upfront, rather than the usual closing assertions, and fail immediately upon receiving unexpected calls. Finally, a call to mock.verify() verifies that indeed all expectations are met.

Creating mocks

To create mocks you can either create anonymous mock functions, like so:

"test should call event handler": function () {
  var mock = sinon.mock();
  var myElement = document.getElementsByTagName("a")[0];

  dom.addEventListener(myElement, "mouseover", mock);
  dom.fireEvent(myElement, "mouseover");

  mock.verify();
}

This test will fail immediately if the mock is called more than once, and the closing verify() call will throw an exception if it wasn't called at least once.

You can also mock methods on objects like we did we the spies and stubs before. The interface is slightly different because we need to create 1) a mock object and 2) expectations on the methods we want to test.

var mock = sinon.mock(jQuery);
mock.expects("each").once().callsArgWith(1, {}).returns({});

This creates an expectation that jQuery.each is called once, and only once, and also instructs the mock to behave as the stub from before.

Controlling mock expectations

Sinon's mocks support both the spy and stub interfaces, although the spy interface isn't as interesting with mocks. Instead, you will typically use one of the following methods to state expectations upfront. Note that these also return the expectation, enabling you to chain them, resulting in good descriptions of the compound expectation.

Additionally, mocks inherit the restore method, to restore the original method. However, in most cases you won't need it as verify restores the method after verifying all expectations.

The following is an example I used in the book. I built a comet client that uses long polling to stream data from the server. The following test expects that calling the connect method on the comet client in turn calls the ajax.poll method with the specified url.

"test connect should start polling": function () {
  var client = Object.create(ajax.cometClient);
  client.url = "/my/url";
  var mock = sinon.mock(ajax);
  mock.expects("poll").once().withArgs("/my/url").returns({});

  client.connect();

  mock.verify();
}

You will probably note that the mock.verify() call towards the end there is the kind of scaffolding a good integration with the test framework can do away with, and I'd agree with you. I'll show what Sinon offers to in this area in the next post.

Status

Currently, everything showed in this post is functional, well tested and possible to use. If you want to try it, you can find a preview version of Sinon here. Also, the full source is available on Gitorious. Documentation is sorely lacking, as the post you just read is currently all there is. There are more features ready for use as well, which will be covered in upcoming posts. Finally, I'm working on tools to test/stub/mock timers and XMLHttpRequest, but they're not done yet.

Seeing as the interface is currently still being beaten into shape I hope some of you take the opportunity to tell me what's looking good, where the API fails, what's lacking, and other ideas/feedback you may have. Leave a comment, shoot me an email at christian@cjohansen.no or holler at me on Twitter (@cjno).

Possibly related

2006 - 2012 Christian Johansen Creative Commons License