Spying on methods

How to spy on methods using sinon.js

JavaScript is very dynamic language - nothing is cast in stone, not even the console object. You can add, delete and replace console methods at will. It might be a bad idea, but it is very simple. Let's extend console by adding console.foo() method that just prints string foo on the screen.

1
2
3
4
5
console.foo = function () {
console.log('foo');
};
console.foo();
// prints 'foo'

In order to properly unit test the added method, we need to make sure it calls console.log with right arguments. The only way to test this is by spying on the log method.

Spying ourselves

We can replace the default console.log method with our own implementation inside the unit test. In this example I will use mocha, but the choice of test runner is irrelevant.

1
2
3
4
5
6
7
8
9
10
11
12
it('calls console.log', function () {
// save reference to the actual console.log method
var prev = console.log;
var called;
console.log = function (arg) {
called = true;
};
console.foo();
// restore console.log
console.log = prev;
assert(called);
});

console.log is a simple function, it does not access its owner console object, so we do not need to worry about binding the context this when storing reference prev = console.log. Otherwise we would need to do prev = console.log.bind(console) call.

It is better to install the fake spy method and restore it in separate setup / teardown functions, rather than inside a unit test, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
describe('console.foo', function () {
var prev, called;
beforeEach(function () {
prev = console.log;
called = false;
console.log = function () {
called = true;
};
});
afterEach(function () {
console.log = prev;
});
it('calls console.log', function () {
assert(!called);
console.foo();
assert(called);
});
});

First rule to remember:

Cleanup the spies at the same logical level as their setup.

For example, if you setup a spy inside a unit test, clean it up inside the unit test. If you setup a spy at a suite level (inside beforeEach), then do not clean it up inside the unit test. Keeps you test code much easier to understand.

Second rule:

Do not disable the original functionality. Call the method spied on.

In the example above, we should really call the original console.log to pass along the arguments, even to see any messages from the assertions inside the unit test. We can easily do this using JavaScript apply function call

1
2
3
4
5
6
7
8
9
beforeEach(function () {
prev = console.log;
called = false;
console.log = function () {
called = true;
var args = [].slice.call(arguments, 0);
prev.apply(null, args);
};
});

Spying by wrapping

Instead of creating temp function and calling the original method ourselves, let's use meld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var meld = require('meld');
describe('console.foo with meld', function () {
var remover, called;
beforeEach(function () {
called = false;
remover = meld.before(console, 'log', function () {
called = true;
});
});
afterEach(function () {
remover.remove(); // restores console.log
});
it('calls console.foo', function () {
assert(!called);
console.foo();
assert(called);
});
});

Professional spying with sinon.js

In the previous examples we only kept track of two pieces of information: the function wrapper and if it was called. You can imagine how number of temp variables would grow if we wanted to check if console.log was called the right number of times and with correct arguments (only foo must be passed). Implementing all these features is unnecessary; let's use sinon.js that implements all these features and more. To use sinon.js in the browser, download the latest built version of the library from sinon website by clicking on "Download Sinon.js " button.

You can get a sense how powerful this library is by opening node REPL after installing sinon and inspecting a spied upon method. In this case, the original function has been augmented with lots of utility methods, mostly used for keeping track how many times it was called, each call's arguments and the returned values.

$ node
> var sinon = require('sinon')
> console.log
[Function]
> sinon.spy(console.log)
{ [Function: proxy]
  reset: [Function],
  invoke: [Function: invoke],
  getCall: [Function: getCall],
  getCalls: [Function],
  calledBefore: [Function: calledBefore],
  calledAfter: [Function: calledAfter],
  withArgs: [Function],
  matches: [Function],
  printf: [Function],
  calledOn: [Function],
  alwaysCalledOn: [Function],
  calledWith: [Function],
  calledWithMatch: [Function],
  alwaysCalledWith: [Function],
  alwaysCalledWithMatch: [Function],
  calledWithExactly: [Function],
  alwaysCalledWithExactly: [Function],
  neverCalledWith: [Function],
  neverCalledWithMatch: [Function],
  threw: [Function],
  alwaysThrew: [Function],
  returned: [Function],
  alwaysReturned: [Function],
  calledWithNew: [Function],
  alwaysCalledWithNew: [Function],
  callArg: [Function],
  callArgWith: [Function],
  callArgOn: [Function],
  callArgOnWith: [Function],
  yield: [Function],
  invokeCallback: [Function],
  yieldOn: [Function],
  yieldTo: [Function],
  yieldToOn: [Function],
  formatters:
   { c: [Function],
     n: [Function],
     C: [Function],
     t: [Function],
     '*': [Function] },
  spyCall: undefined,
  called: false,
  notCalled: true,
  calledOnce: false,
  calledTwice: false,
  calledThrice: false,
  callCount: 0,
  firstCall: null,
  secondCall: null,
  thirdCall: null,
  lastCall: null,
  args: [],
  returnValues: [],
  thisValues: [],
  exceptions: [],
  callIds: [],
  displayName: 'spy',
  toString: [Function: toString],
  _create: [Function: create],
  id: 'spy#0' }

Let's use this in our test suite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe('console.foo with sinon', function () {
var sinon = require('sinon');
beforeEach(function () {
sinon.spy(console, 'log');
});
afterEach(function () {
console.log.restore();
});
it('calls console.foo', function () {
assert(!console.log.called);
console.foo();
assert(console.log.called);
});
it('calls console.foo with right argument', function () {
console.foo();
assert(console.log.calledOnce);
assert(console.log.firstCall.calledWith('foo'));
});
});

sinon.js is quick, simple and powerful, and we have only used one feature. There are also stubs, mock XMLHttpRequests and fake timers. It is a must library for JavaScript testing.