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:
- Standalone There's no reason to tie stubbing and mocking to a specific testing library. Granted, some aspects can be automated when doing so, but such integration can be provided for standalone libraries as well.
- Minimal global footprint The test environment is the last environment you want massive global pollution. The stub and mock API should live within a single object.
- Easy to use That's probably a given, but some of the mocking solutions I've seen requires unnecessary amounts of scaffolding to get going.
- Easy to integrate Making up for being standalone, I want the (minimal) scaffolding to be easy to automate with any testing library.
- CommonJS compliant Work both in browsers and on the server using CommonJS modules.
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.
-
jQuery.ajax.calledboolean,true -
jQuery.ajax.callCountnumber,1 -
jQuery.ajax.calledOnceboolean, convenience,true -
jQuery.ajax.calledTwiceboolean, convenience,false -
jQuery.ajax.calledThriceboolean, convenience,false
Call data
Spies store data about calls in four arrays. If you prefer to keep things low-level, these are all you need.
-
jQuery.ajax.args
A nested array of arguments received by the spy. To retrieve the first argument of the first call, you'd dojQuery.ajax.args[0][0]where the first 0 is the call number and the second is the argument index. -
jQuery.ajax.returnValues
An array of each call's return value. To retrieve the value returned the first time the spy was called, you'd dojQuery.ajax.returnValues[0]. For calls that don't have an explicit return value, the array storesundefined. -
jQuery.ajax.exceptions
An array of exceptions thrown by the spy. If the spy does not throw an exception in any given call,undefinedis stored in the array. The array always contains one entry per call, regardless of whether it throws or not. -
jQuery.ajax.thisValues
An array ofthisvalues. For each call, the spy stores the value ofthisin this array, which can be useful when testing that functions are passed callbacks bound to a specific object, or that callbacks are called with the right context usingcallorapply.
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.
-
jQuery.ajax.calledWith(arg1, arg2, ...)
Pass in any number of arguments, and the method returnstrueif it was called at least once with the specified arguments. The list of arguments does not need to be exhaustive - you can provide the first argument, and the method will returntrueif any one call received this argument as its first (possibly with additional arguments). -
jQuery.ajax.calledWithExactly(arg1, arg2, ...)
If you need to know if a function received certain arguments, and nothing but those certain arguments, this method is the strict variation ofcalledWith(arg1, arg2, ...). -
jQuery.ajax.alwaysCalledWith(arg1, arg2, ...)
Same ascalledWith(arg1, arg2, ...), only it requires all the calls to the spy to match the arguments. -
jQuery.ajax.alwaysCalledWithExactly(arg1, arg2, ...)
Same ascalledWithExactly(arg1, arg2, ...), only it requires all the calls to the spy to match the arguments and nothing but them. -
returned(returnValue)
Returnstrueif the spy returned the specified return value. -
alwaysReturned(returnValue)
As above, only match all calls. -
threw(exceptionOrType)
If no arguments are passed, the method returnstrueif the spy threw an exception (in any one call). If a string argument is passed, it returnstrueif the spy threw an exception of the provided type, such as"TypeError". Finally, if an exception object is passed,trueis returned if the spy threw this exact exception object (not likely to be used much, but there for completeness).I'm thinking that this method probably should accept two strings, denoting
the type and message expected of the thrown exception. Possibly it could even accept a string type and regular expression to match the message. What do you think? -
alwaysThrew(...)
As above, only match all calls. -
calledOn(thisObj)
Returnstrueif the method was called with the specified object asthis. -
alwaysCalledOn(...)
As above, only match all calls.
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:
- To avoid some inconvenient interface - for instance to avoid making actual requests to a server from tests.
- 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
-
returns(returnValue)
Causes the stub to return the specified value. -
throws(exception)
Causes the stub to throw an exception. If the argument is a string, it specifies the type of exception to throw. If it's an object, it throws the argument untouched. If no argument is provided, anErroris thrown (when called). -
callsArg(index)Will probably change! This method causes the stub to immediately call functions passed to it. The argument specifies which argument number (0 based index) to treat as a callback. I'm not happy with the name or the numeric index for this method. Suggestions are welcome.
-
callsArgWith(index, arg1, arg2, ...)Will probably change! Like the above method, only here you can also provide arguments the stub will pass to the callback. As with
callsArg, I'm not sure about the signature of this method. I want something more intuitive, but this is the best I've come up with so far.
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.
-
expectation.atLeast(callCount)
Makes the mock accept any number of calls, so long as it is called at least the specified number of times. -
expectation.atMost(callCount)
Makes the mock accept any number of calls, so long it is not called more than the specified number of calls. -
expectation.exactly(callCount)
Specify the exact number of calls to satisfy the expectation. -
expectation.never()
Shortcut forexactly(0) -
expectation.once()
Shortcut forexactly(1) -
expectation.twice()
Shortcut forexactly(2) -
expectation.thrice()
Shortcut forexactly(3) -
expectation.withArgs(arg1, arg2, ...)
Like the spy methodcalledWith(arg1, arg2, ...), only for upfront expectations. -
expectation.withExactArgs(arg1, arg2, ...)
Like the spy methodcalledWithExactly(arg1, arg2, ...), only for upfront expectations. -
expectation.on(thisObj)
Expects thethisvalue of the call to match the specified object.
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).
Comments are closed