Sinon.JS is a popular test double library that provides spies, stubs, mocks, fake servers, and various helpers. We will introduce two Sinon.JS interfaces in this chapter—spies and the sandboxed test helper—and discuss the rest in Chapter 5, Test Stubs and Mocks.
Sinon.JS provides extensible test spies that can record many different aspects of a function execution, including calling parameters, return values, and thrown exceptions. The basic developer workflow is to create a spy, hook it into a function under test, execute the function, and then verify that the spy's recorded information matches with the test expectations. In this section, we will walk through the different ways to create spies and discuss some of the most useful parts of the Sinon.JS spy API.
Spies can be created as anonymous standalone functions, which are often used to test event logic in Backbone.js applications. For example, we create a Backbone.js event object and an anonymous Sinon.JS spy in the following code. The spy listens to the foo
event, which we trigger. Then, we can inspect the spy and assert that the spy was called once and passed 42
as a parameter:
it("calls anonymous spy on event", function () { var eventer = _.extend({}, Backbone.Events), spy = sinon.spy(); // Set up the spy. eventer.on("foo", spy); expect(spy.called).to.be.false; // Fire event. eventer.trigger("foo", 42); // Check number of calls. expect(spy.calledOnce).to.be.true; expect(spy.callCount).to.equal(1); // Check calling arguments. expect(spy.firstCall.args[0]).to.equal(42); expect(spy.calledWith(42)).to.be.true; });
Sinon.JS provides assertion helpers for many spy methods and properties with the sinon.assert
object. In the previous example, we used Chai assertions to verify the spy's recorded information. But, we could have equivalently used Sinon.JS assertions as follows:
it("verifies anonymous spy on event", function () { var eventer = _.extend({}, Backbone.Events), spy = sinon.spy(); eventer.on("foo", spy); sinon.assert.notCalled(spy); eventer.trigger("foo", 42); sinon.assert.callCount(spy, 1); sinon.assert.calledWith(spy, 42); });
The sinon.assert
helpers have an advantage over most equivalent Chai assertions for the reason that the failure messages are informative and specific. For example, a failing assertion for sinon.assert.calledWith(spy, 42)
produces the error message AssertError: expected spy to be called with arguments 42
.
Sinon.JS spies can wrap existing functions as well. In the following example, we wrap the function divide
with a spy, producing divAndSpy
. Then, we can call divAndSpy
in any manner that we could for divide
. We can also inspect spy properties such as calledWith()
in the wrapped spy:
it("calls spy wrapper on function", function () { var divide = function (a, b) { return a / b; }, divAndSpy = sinon.spy(divide); // Call wrapped function and verify result. expect(divAndSpy(4, 2)).to.equal(2); // Now, verify spy properties. sinon.assert.calledOnce(divAndSpy); sinon.assert.calledWith(divAndSpy, 4, 2); // Sinon.JS doesn't have assert for returned. expect(divAndSpy.returned(2)).to.be.true; });
Finally, Sinon.JS spies can wrap methods in objects. This is a particularly powerful means of spying on one method within an overall class or Backbone.js component to gather information throughout the entire execution path. The wrapped object methods contain Sinon.JS spy properties, meaning that we do not have to separately track a spy variable.
Wrapped object methods remain spies until unwrapped with the restore()
function, which removes the spy and reinstates the original function. As an example, let us consider the following object with two methods:
var obj = { multiply: function (a, b) { return a * b; }, error: function (msg) { throw new Error(msg); } };
We can spy on multiply
to verify its call and return values, and spy on error
to check that it throws an expected exception. In both cases, we call the wrapped object method directly (for example, obj.multiply()
) and then access the method spies. Finally, we need to call restore()
at the end of the test to unwrap the spies on obj
:
it("calls spy on wrapped object", function () { // Wrap members with `sinon` directly. sinon.spy(obj, "multiply"); sinon.spy(obj, "error"); expect(obj.multiply(5, 2)).to.equal(10); sinon.assert.calledWith(obj.multiply, 5, 2); expect(obj.multiply.returned(10)).to.be.true; try { obj.error("Foo"); } catch (e) {} sinon.assert.threw(obj.error, "Error"); // Have to restore after tests finish. obj.multiply.restore(); obj.error.restore(); });
One issue with the previous example spec is that if an assertion fails before restore()
is called, the object methods are still wrapped in the spies. If any subsequent (and otherwise passing) test tries to wrap an already wrapped method, Sinon.JS will throw an error such as TypeError: Attempted to wrap <function name> which is already wrapped
.
Thus, it is important to ensure that each spy eventually calls restore()
, regardless of whether or not the underlying test has passed. One way of achieving this is with a try
/finally
block in the test. Another way is to create spies in a before
function and call restore()
on them in an after
function. However, the easiest and the most flexible way is to perhaps use the sinon.test
sandbox function.
Sinon.JS provides an execution environment dubbed as a sandbox that can be configured with spies, stubs, mocks, and other fake objects (for example, fake timers and AJAX requests). Conveniently, all faked properties and methods can be unwound with a single restore()
call on the sandbox object.
Reviewing the Sinon.JS sandbox documentation at http://sinonjs.org/docs/#sandbox is highly recommended. There are some subtle pitfalls and surprises related to how an application execution changes within a sandbox. For example, the default sandbox will fake time and the related functions such as setTimeout
. It means that if your code waits for 10 milliseconds before execution, it will not run until the developer manually advances the time in the fake clock
object.
The sinon.test
wrapper function takes this one step further by creating a default sandbox, which is automatically restored after the wrapped code finishes its execution. Repeating our previous object method example with sinon.test
yields a more elegant version of the spec, in which we don't manually call restore()
on the spies and still guarantee that the wrapped object methods are restored:
it("calls spy with test helper", sinon.test(function () { // Wrap members using context (`this`) helper. this.spy(obj, "multiply"); this.spy(obj, "error"); expect(obj.multiply(5, 2)).to.equal(10); sinon.assert.calledWith(obj.multiply, 5, 2); expect(obj.multiply.returned(10)).to.be.true; try { obj.error("Foo"); } catch (e) {} sinon.assert.threw(obj.error, "Error"); // No restore is necessary. }));
While the sinon.test
helper is a handy tool for managing Sinon.JS objects, it is not always an appropriate choice for every spec. For example, asynchronous Mocha tests are tricky because sinon.test
can potentially restore the entire sandbox before the done()
parameter is later called in the test code. Additionally, a side effect of using sinon.test
is that the Mocha test reporter will no longer show the test code when clicking on a spec description in the test driver web page. The reason for this makes sense—sinon.test
wraps the actual test function, so sinon.test
is all that the Mocha reporter sees. It is ultimately a matter of developer intuition and experience as to when to use the sinon.test
shortcut. In this book we use the wrapper for a subset of our synchronous Sinon.JS-based specs.
Sinon.JS spies provide a fairly comprehensive set of properties and methods for inspecting execution information (see http://sinonjs.org/docs/#spies for a complete list). Spies can be inspected generally to see if an argument or return value was ever encountered during execution or specifically to check information for a single function call.
An introductory set of useful spy methods and properties includes:
spy.callCount(num)
: This returns the number of times the spied function was called. This is available as an assertion with sinon.assert.callCount(spy, num)
.spy.called
: This is true
if the function was called one or more times. Sinon.JS also provides properties to verify a few specific call counts, for example, spy.calledOnce
. Assertion versions include sinon.assert.called(spy)
, sinon.assert.notCalled(spy)
, and sinon.assert.calledOnce(spy)
.spy.calledWith*
/spy.notCalledWith*
: Sinon.JS provides methods that can verify if a spy was sometimes/always called with expected parameters. For example, spy.calledWithExactly(arg1, arg2)
checks whether the function was called one or more times with arg1
and arg2
. By contrast, spy.alwaysCalledWith(arg1)
checks whether every function call had a first argument arg1
and any number of additional arguments.spy.returned(obj)
/spy.alwaysReturned(obj)
: This returns true
if obj
was returned by the function one or more times/on every call.Sinon.js spies also record thrown exceptions, which can be inspected with the following methods:
spy.threw()
: This returns true
if the function threw an exception one or more times. The spy.alwaysThrew()
alternative returns true
if the exception was thrown every time. Both can take optional arguments of the type string (for example, "Error"
) or an actual error object to additionally require a type match for the exception. Assertion versions are sinon.assert.threw(spy)
and sinon.assert.alwaysThrew(spy)
respectively.Each time a spied function is called, Sinon.JS stores a call object with relevant information in an internal array. Call objects are useful in situations where a spied function is executed many times, but only one specific call needs to be inspected.
Call objects can be accessed from a spy in various ways:
spy.getCall(n)
: Retrieves the nth call object of the spy from a zero-indexed array. For example, spy.getCall(1)
retrieves the call object from the second time the spied function was called.spy.firstCall
, spy.secondCall
, spy.thirdCall
, and spy.lastCall
: These are helper properties that access commonly used call objects.Call objects provide methods and properties for the particular function call they wrap:
spyCall.calledOn(obj)
: This returns true
if obj
was the context (this
) variable for the call. The this
variable's value is also available directly from the property spyCall.thisValue
.spyCall.calledWith*
/spyCall.notCalledWith*
: These are spy call methods that verify if a single call was/was not made with specific arguments. It parallels the spy API methods, which instead check all the function calls and not just one. The call object also provides the specific arguments the function was called with, in the property spyCall.args
.spyCall.returnValue
: This is the property containing the return value for the function call.spyCall.threw()
: This returns true
if the function call threw an exception. The exception object itself is available as spyCall.exception
.44.200.94.150