Getting to know Sinon.JS

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.

Spying on functions with Sinon.JS

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.

Anonymous spies

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;
});

Spy assertions

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.

Function spies

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;
});

Object method spies

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();
});

Playing in the sandbox with Sinon.JS test helpers

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.

Tip

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.

Delving into the Sinon.JS spy API

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.

The spy API

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.

The spy call API

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.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.226.239