Unit Test like a Secret Agent with Sinon.js
June 4, 2013
Introduction
Sinon.js is a really helpful library when you want to unit test your code. It supports spies, stubs, and mocks. The library has cross browser support and also can run on the server using Node.js.“Standalone test spies, stubs and mocks for JavaScript. No dependencies, works with any unit testing framework.”
Spies
“A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. A test spy can be an anonymous function or it can wrap an existing function.”
Example
A test spy records how it is used. It will record how many times it was called, what parameters were used, when it was called, and a bunch of other things. Here you can see an example of creating a spy and I've listed out only a small subset of it’s features such as called
, callCount
, calledWith
, threw
, returned
, and more.
var callback = sinon.spy();
callback(); // Invoke the spy callback function
callback.called;
callback.callCount;
callback.calledWith(arg1);
callback.threw();
callback.returned(obj);
callback.calledBefore(spy);
callback.calledAfter(spy);
/* ... more ... */
In addition to just creating a new spy, you can also take an existing function and turn it into a spy. In this example we are taking jQuery and turning it’s ajax
method into a spy. Once the spy has been used you can actually pull out one of those instances and verify how that particular call was used. And again, it is important to restore the function back to it’s original state much like we did when we manually stubbed our functions previously.
sinon.spy($, "ajax");
$.ajax({ / ... / }); // Call spy version of jQuery.ajax
var call = $.ajax.getCall(0);
call.args;
call.exception;
call.returnValue;
$.ajax.restore();
Mission Impossible: Spy
In the following simple code example we are creating a newethanHunt
spy and passing it to the missionImpossible.start
method.As you can see the start
method takes the agent that was passed in and immediately invokes it.
The spy will record how it is used and then you can observe what happened.
var missionImpossible = {
start: function (agent) {
agent.apply(this);
}
};
// By using a sinon.spy(), it allows us to track how the function is used
var ethanHunt = sinon.spy();
missionImpossible.start(ethanHunt);
At this point we can interrogate ethanHunt
if he was called or not, how many times it was called, and a bunch of other questions.
> ethanHunt.called
true
> ethanHunt.calledOnce
true
> ethanHunt.callCount
1
Stubs
“Test stubs are functions (spies) with pre-programmed behavior. They support the full test spy API in addition to methods which can be used to alter the stub's behavior.”
A stub in Sinon.js is also a spy as we've just seen, but it is also a function that has some predefined behavior. A stub is used when we want to fake some functionality so that our system thinks everything is performing normally.
Example
You'll see here that after we have created a stub we can optionally respond to it based on the parameters that are passed to it.
var stub = sinon.stub(),
opts = { call: function (msg) { console.log(msg); } };
// We can control how the sinon.stub() will behave based on how it’s called!
stub.withArgs("Hello").returns("World");
stub.withArgs("Wuz").returns("Zup?");
stub.withArgs("Kapow").throws();
stub.withArgs(opts).yieldsTo("call", ["Howdy"]);
stub("Hello"); // "World"
stub(options); // "Howdy"
/* ... more ... */
Here we are telling our stub that if "Hello"
is passed to it that it should return the string "World"
and if we pass "Wuz"
to the stub that "Zup?"
should be returned.
We can do other things like if "Kapow"
is passed to our stub then an exception will be thrown and we can get even more sophisticated and say if an object is passed to the stub it should yieldTo
(or invoke) the call
function that was passing using the "Howdy"
argument. This is some pretty serious and awesome functionality built into these stubs!
Mission Impossible: Stub
In this next mission, if you choose to accept it... we are stubbing out atape
function that will be passed into an assignment method.The tape will either be passed the string "accept"
or "reject"
and depending on the answer we want a different result.
With a sinon stub, that is no problem. We can just say tape.withArgs("accept"). returns(new Mission())
and if we wanted to throw a Disintegrate
exception if the tape was rejected then we just follow the same pattern... tape.withArgs("reject"). throws("Disintegrate")
.
If you can't tell already these stubs are really powerful and a great addition to your testing toolkit.
var missionImpossible = {
numberOfAssignments: 0,
assignment: function (answer, tape) {
var mission = tape(answer);
this.numberOfAssignments++;
return mission;
}
};
function Mission() { }
var tape = sinon.stub();
tape.withArgs("accept").returns(new Mission());
tape.withArgs("reject").throws("Disintegrate");
Once we've set up our stub, we can exercise our code as we would normally and the stub will respond with whatever behavior we predefined. Below you'll see that once we pass "accept"
that we are getting a Mission
object back and if we "reject"
the assignment that a Disintegrate
exception is thrown.
> missionImpossible.assignment("accept", tape);
Mission {}
> missionImpossible.numberOfAssignments
1
> missionImpossible.assignment("reject", tape);
⊗ Disintegrate
Stubbed Unit Test
Let’s take an example Twitter unit test and show how we can use a stub to simulate a response from jQuery’s ajax
method.
describe("getTweets", function () {
var fakeData = [
{
created_at: "Fri Apr 05 19:39:30 +0000 2013",
text: "tweet 1",
retweeted: false,
favorited: false,
user: { name: "name 1" }
},
/* ... */
];
before(function () {
sinon.stub($, "ajax").yieldsTo("success", fakeData);
});
it("should $.ajax & invoke callback", function (done) {
twitter.getTweets("elijahmanor", function (tweets) {
expect(tweets.length).to.be(5);
done();
});
});
after(function () { $.ajax.restore(); });
});
In the before hook we will ask Sinon.js to create us a new stub based off of jQuery’s ajax
method and we want to yieldTo
(or invoke) the success function from the object that is passed to it. And while we are at it we want to pass our fake twitter data along with the success
function.
With that one line of code we have stubbed out the jQuery ajax
method and provide fake test data that we can use in our unit test.
Again, it is important to clean up after ourselves so in the after hook at the bottom here we are taking the jQuery.ajax
method and calling restore which removes all of the stub behavior from the function,
Mocks
“Mocks (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations. A mock will fail your test if it is not used as expected.”
Now we finally get to mocks. Mocks are a lot like a stub and a spy, but with a slight twist. With a mock you define up front all of the things you want to expect ( or happen ) then when you are all done with your tests you assert that all those things happened as planned. So, it’s a slightly different way to think than if using a spy or stub by themselves.
Example
In the following code we are defining a mock based off our opts object and we are saying that we expect the call method should only be called once and when it is called that it should have the "Hello World"
string argument passed to it.
var opts = { call: function (msg) { console.log(msg); } },
mock = sinon.mock(opts);
// You state your success criteria upfront
mock.expects("call").once().withExactArgs("Hello World");
/* ... twice, atMost, never, exactly, on, etc ... */
opts.call("Hello World");
mock.verify();
mock.restore();
Then we proceed to run our code that we want tested. You'll see here I’m calling the call method passing the "Hello World"
string.
And then at the end you tell the mock object to mock.verify()
that all of the expectations you've made previously are valid. If for some reason an expectation was not met, then an exception will occur. And then just like in most of the other examples, we need to clean up after ourselves and call the restore method off of what was mocked.
Mocked Unit Test
Let’s take another look at the Twitter getTweets
unit tests again, but this time use a mock instead of a stub.
describe("getTweets", function () {
var mock, fakeData = [];
before(function () {
mock = sinon.mock(jQuery).expects("ajax").once()
.yieldsTo("success", fakeData);
});
it("should call jQuery.ajax", function (done) {
twitter.getTweets("elijahmanor", function (tweets) {
mock.verify();
done();
});
});
after(function () { jQuery.ajax.restore(); });
});
In the before hook I’m creating a mock of the jQuery object and I’m expecting that the ajax method will only be called once and that it should invoke the success
method of the object I pass in with some fakeData
I've provided.
Inside my unit test I run the code I want to tests, which is the getTweets
method, and then on the callback I call the verify
method off of the mock to make sure my expectations have been met.
And as before I restore the object in the after hook.
Fake Timers
“Fake timers is a synchronous implementation of setTimeout and friends that Sinon.JS can overwrite the global functions with to allow you to more easily test code using them.“
Another handy feature of Sinon.s is that you can fake timers! At first this might seem strange, but it turns out it is really powerful and clever.
Example
We first start by asking Sinon.js to useFakeTimers()
and save off the clock it gives us. Now let’s take some jQuery animation code that will fadeIn
an element slowly onto the screen.
var clock = sinon.useFakeTimers();
var hidden =
$("<div hidden="">Peekaboo</div>")
.appendTo(document.body).fadeIn("slow");
clock.tick(650); // slow = 600ms
hidden.css("opacity") === 1; // true
clock.restore();
Normally if we wanted to test if this element showed up on the screen we'd either need to provide a callback when the animation is finished or tap into the promise created from the deferred and wait for that to resolve.
However, much like a time lord we can take sinon’s TARDIS, errr... I mean fake timer and tell the clock that we are now 650 milliseconds in the future! And then we can immediately assert that the element is visible without waiting. And of course we will need to restore the clock back to normal when we are done.
Fake Server
“High-level API to manipulate FakeXMLHttpRequest instances.”
Another neat feature that Sinon.js has is a fake server. This is a high level abstraction over the FakeXMLHttpRequest
that Sinon.js also provides if you need more granular support.
Example
We can create a fake server from Sinon.js, and we can define that for a GET to the /twitter/api/user.json resource we want to respond with a status code of 200 and the following JSON data.
var server = sinon.fakeServer.create();
server.respondWith("GET", "/twitter/api/user.json", [
200,
{"Content-Type": "application/json"},
'[{"id": 0, "tweet": "Hello World"}]'
]);
$.get("/twitter/api/user.json", function (data) {
console.log(data); // [{"id":0,"tweet":"Hello World"}]
});
server.respond();
server.restore();
Then if we called jQuery’s get
method with the same URL then we'd get back the data we stubbed out. A key to remember is that you do need to tell the server to respond as we did immediately after we called the get method. And finally we need to restore the server when we are done.
Fake Server Unit Test
Let’s take this technique and add it to our twitter unit test.
describe("getTweets - Server", function () {
var server, fakeData = [ /* ... */ ];
before(function () {
// Doesn’t work :( It’s JSONP!
server = sinon.fakeServer.create();
server.respondWith(
"GET",
"https://api.twitter.com/.../elijahmanor.json?count=5",
[200, { "Content-Type": "application/json" }, JSON.stringify(fakeData)]
);
});
it("should $.ajax & invoke callback", function (done) {
twitter.getTweets("elijahmanor", function (tweets) {
expect(tweets.length).to.be(5);
done();
}); server.respond();
});
after(function () { server.restore(); });
});
In our before
hook we create the server and match the resource that our twitter app will be calling and pass back the data we want to stub out. Then we unit test out the getTweets
method as we did before, but things don't work out as we expect! Why is that? Well, it is because we are using JSONP as our jQuery ajax datatype. The way JSONP works is that it isn't actually using XMLHttpRequest
as a typical Ajax call does. Instead JSONP uses some trickery of injecting a dynamic script tag on your page and a bunch of other things that jQuery tries to hide from you for simplicities sake.
So, in this case using the fake server won't help us. It would be better if we used a stub like we did in the last example.
Conclusion
Hopefully you can see that Sinon.js is a great utility library to help make unit testing a much more effective and terse experience. You'll probably more often than not find yourself making spies and stubs much more often than mocks, but that is really up to how you approach unit testing.
If you enjoyed this content you can get more from my recent Pluralsight course entitled: Front-End First: Testing and Prototyping JavaScript Apps where I cover an introduction to Unit Testing, look at various examples of hard to test code and introduce the following libraries and tools... Mocha, Grunt, Mockjax, amplify.request, mockJSON, etc...
Tweet about this post and have it show up here!