Test Spies, Stubs and Mocks - Part 1.5

While presenting the core Sinon interface yesterday i unintentionally left out parts of the API. To make up for the mistake, here are the missing pieces.

Spies

In the original post I wrote about the data spies collect in arrays as well as the methods spies expose to interact with the recorded call data. However, you might have recognized a gap in the interface - you can either match any one call, or all of them. If you need more fine-grained control, spies have a getCall(num) method which retrieves a specific call, which in turn supports a similar interface to that of the spies themselves.

Behavior verification for individual calls

Given the original example:

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

You can retrieve the first (and in this case, only) call with:

var call = jQuery.ajax.getCall(0);

Then you can match against its recorded data using:

Basically, each call support the same exact interface as the stub, save for the "always" variation. When called on a specific call, the methods naturally match only that one call.

These methods are used extensively in the implementation of spies, but I'd be careful with using them extensively in tests. Targeting specific calls like this may cause your tests to be unnecessarily implementation specific, thus brittle.

Call order

I also left out two methods on spies: calledBefore and calledAfter. Both of these can come in handy in certain situation, but are also subject to the same reservation I just mentioned: exaggerated use will cause too implementation specific tests.

To keep with the jQuery examples, the following test could be imagined to be part of jQuery's test case for the ajax method.

"test using XMLHttpRequest open should be called before send": function () {
  var open = sinon.stub(XMLHttpRequest.prototype, "open");
  var send = sinon.stub(XMLHttpRequest.prototype, "send");

  jQuery.ajax("/some/url");

  assert(open.calledBefore(send));
  assert(send.calledAfter(open));
}

The two asserts in the above test case are basically testing the same, which one to use is a matter of preference.

Stubs

I left out a little detail from the stub interface as well. As you may have noticed, stubs, mocks and spies all are function-centric in Sinon. From my own experience, I very rarely need to fake entire objects. A single test rarely need to fake out more than a handful of functions at a time, so focusing on functions makes sense.

For those rare cases where you really want to stub all the methods of an object, you can use the sinon.stub() function with only an object as first argument. Doing so instructs Sinon to stub out all the methods. If you need you can fine-tune the stubs later, as seen in the following example:


sinon.stub(XMLHttpRequest.prototype);
XMLHttpRequest.prototype.getResponseHeader.returns("");

jQuery.ajax("/some/url");

// ...

Here, Sinon stubs all the methods on XMLHttpRequest.prototype. This means they're all stub functions now, allowing you call the stub and spy methods on them directly to further tweak their behavior.

There's no equivalent to this for mocks. If you really want to create mock expectations on all the methods of an object in a test you might want to reconsider your test design - chances are it's doing more than it should. I have no plans to implement a way to mock and set expectations on more than a single method at a time as I don't see a viable use case for it.

Note that you can mix and match as well. If you want to silence an object and set an expectation on one or a few of the methods, you can do that:

"test should always pass argument to send": function () {
  sinon.stub(XMLHttpRequest.prototype);
  var mock = sinon.mock(XMLHttpRequest.prototype);

  // If no argument is passed to send on GET requests, FF <= v3.0 will
  // throw an exception
  mock.expects("send").once().withArgs(null);

  jQuery.ajax("/some/url");

  mock.verify();
}

The above test silences all the native XMLHttpRequest methods, and then overrides the stub created for send with a mock expectation that expects an argument to be passed.

That's about it. In the next post, which is the real second post, I'll discuss integration with xUnit style testing frameworks.

Published 26. May 2010 in javascript, test driven development, open source og sinon.js.

Possibly related