Chapter 5. Test Stubs and Mocks

With the integration of Sinon.JS into our test infrastructure, we now have deeper insight into the methods and actions within our Backbone.js application. As we turn to testing the remaining application components, we will move beyond merely observing methods during tests and actually replace method behaviors.

Sinon.JS has us covered in this respect as well—the library provides solid support for functional behavior modifications. Specifically, we can leverage its powerful stub and mock abstractions to reduce Backbone.js component dependencies and cross-application side effects during testing. In this chapter, we will explore these and other Sinon.JS capabilities through the following topics:

  • Using Sinon.JS stubs to replace function behaviors and isolate Backbone.js components in tests
  • Introducing Sinon.JS mocks that spy, stub, and verify application behavior in a single abstraction
  • Writing tests for the remaining components of our Backbone.js application and deciding on the proper Sinon.JS tools for the given test scenarios
  • Investigating other contextually useful Sinon.JS test helpers
  • Faking a remote backend server in Backbone.js collection tests

Replacing method behaviors with Sinon.JS stubs

We have been able to handle our Backbone.js testing dependencies so far with a combination of clever class design and some manual faking. However, we are quickly approaching a point where we need a more reliable and consistent approach.

We will look to stubs to provide a straightforward and predictable means of substituting method behaviors in any Backbone.js component to reduce unintended application side effects and dependency issues. The ability to temporarily replace existing methods during tests offers an enormous amount of flexibility, particularly in situations where:

  • A Backbone.js application is under early development and not all of the planned components exist. Stubs allow us to write a simulated equivalent of the missing functionality in tests that can later be removed when the real application code is written. Even after the application code has been implemented, stubs may still be appropriate for a subset of the original specs, depending on what type of behavior is being tested.
  • An application code is sensitive to the timing of the UI and/or other events.
  • An application depends on external resources such as a database or cloud service.
  • A Backbone.js component has application dependencies and/or interactions that are too complex to manually swap in tests and that must be faked internally.

Getting started with stubs

To kick things off, let's integrate some stubs into an object literal from the previous chapter:

var obj = {
  multiply: function (a, b) { return a * b; },
  error: function (msg) { throw new Error(msg); }
};

In the ensuing spec, we will show you two different ways to stub obj.multiply() with Sinon.JS. In the first call (sinon.stub(obj, "multiply").returns(5)), we use the returns method to always return a hardcoded value. The second stub takes an alternate approach that inserts a replacement function (that adds instead of multiplying). In both cases, we call restore() after the test finishes to keep obj from being permanently modified:

it("stubs multiply", function () {
  // Stub with a hard-coded return value.
  sinon.stub(obj, "multiply").returns(5);
  expect(obj.multiply(1, 2)).to.equal(5);
  obj.multiply.restore();

  // Stub with a function.
  sinon.stub(obj, "multiply", function (a, b) {
    return a + b;
  });
  expect(obj.multiply(1, 2)).to.equal(3);
  obj.multiply.restore();
});

Turning our attention to obj.error() in the ensuing code snippet, we create an empty stub on the object method to prevent the real function from throwing an exception. We don't need a replacement function or a returns value because we just want to avoid the default behavior. Additionally, we use the sinon.test sandbox helper to automatically call restore() on any stubs that were created within the test function:

it("stubs error", sinon.test(function () {
  this.stub(obj, "error");
  expect(obj.error).to.not.throw();
}));

As illustrated in the previous code snippets, we can now easily replace arbitrary methods with different code and/or return values.

The stub API

Sinon.JS stubs implement the entire spy API and provide additional methods that can swap existing application functions with new code and behaviors during our tests. The first step in stubbing is to create a stub object and potentially replace one or more object methods:

  • sinon.stub(): This creates an anonymous stub without any specified behavior.
  • sinon.stub(obj, methodName): This stubs a single object's method with an empty function. This alone is sufficient to replace the underlying code's execution like we saw in the code with obj.error(). Alternatively, you can further call stub API methods to modify return, callback, or other behaviors of the stub.
  • sinon.stub(obj, methodName, fn): This stubs a single object's method with the replacement function provided in the fn parameter.
  • sinon.stub(obj): This replaces all the methods in an object with stubs.

Once we have a stub object, we can enhance it with fake behaviors and responses as applicable in a given testing situation. Some of these methods are appropriate for synchronous (non-callback) function responses:

  • stub.returns(obj): This stub will return the value obj when called.
  • stub.throws(): This stub will throw an Error object exception when called. A specific error will be used if throws() is called with a type string (for example, "TypeError") or an error object (for example, new TypeError()).

Sinon.JS also supports asynchronous callbacks in stubbed methods:

  • stub.yields(arg1, arg2, ...): The first parameter to the stubbed method must be a callback function that the stub will call with the parameters arg1, arg2, and so on. In the following code snippet, we'll stub obj.async() and use yield() to inject the fake arguments 1 and 2 into the callback:
    it("stubs with yields", function (done) {
      var obj = {
        async: function (callback) { callback("a", "b"); }
      };
    
      sinon.stub(obj, "async").yields(1, 2);
    
      // Verify stub calls with (1, 2), *not* ("a", "b").
      obj.async(function (first, second) {
        expect(first).to.equal(1);
        expect(second).to.equal(2);
    
        obj.async.restore();
        done();
      });
    });
  • stub.yieldsOn(context, arg1, arg2, ...): This is equivalent to stub.yields(), except that it also injects the context parameter as the special variable this when invoking the callback.
  • stub.yieldsTo(property, arg1, arg2, ...): This is also similar to stub.yields(), except that the callback to the underlying method is expected to be an object with a property name matching the property parameter value.
  • stub.yieldsToOn(property, context, arg1, arg2, ...): This is a combination of stub.yieldsOn() and stub.yieldsTo() that uses an object callback property and context variable.
  • stub.callsArgWith(index, arg1, arg2, ...): The stub.yields* collection of methods utilizes the first parameter of the stubbed method. However, asynchronous callbacks can occur in other parameter positions. The stub.callsArgWith() method allows us to specify the index of the callback parameter to be used and the arguments to be passed to the function.
  • stub.callsArg*: In addition to stub.callsArgWith(), the methods stub.callsArg(index), stub.callsArgOn(index, context), and stub.callsArgOnWith(index, context, arg1, arg2, ...) take a first parameter named index that specifies the index of the callback to be invoked in the wrapped method and work in a manner analogous to their yields* counterparts mentioned previously.

This set of stub features is sufficient to cover most Backbone.js testing situations. At the same time, it is worthwhile to review the full Sinon.JS stub API documentation (http://sinonjs.org/docs/#stubs) to learn about additional methods and helpers.

..................Content has been hidden....................

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