Stubbing and mocking

Stubbing and mocking are an important aspect of testing object-oriented programs. jstest provides a simple way to use both techniques when writing your tests. These terms are often confused so I’ll clarify what I mean by them.

Stubbing means replacing a method, function or an entire object with a version that produces hard-coded responses. This is typically used to isolate components from each other, and your code from the outside world. For example, stubbing is often used to decouple tests from storage systems and to hard-code the result of HTTP requests to test code that relies on data from the internet.

Mocking is a form of testing that involves verifying behaviour by checking which methods are called during a test. Like stubbing, it involves replacing methods with fake versions, but it also means setting expectations that those methods must be called. This is used to specify contracts between layers of an application, and to test side-effects.

You can use mocks and stubs at any point during a test and jstest will remove the stub methods at the end of each test, reinstating the original methods.

Stubbing methods

We’ll cover stubbing first because it shares a lot of API with mocking, and is a little simpler. To stub out a method on an object we use the stub() function.

stub(object, 'methodName')
object.methodName() // -> undefined

This is the simplest stub you can make, it means that any call to object.methodName() with any arguments will return undefined and have no side-effects. You can specify a return value using the returns modifier. If you provide multiple return values they will each be used in turn, looping back to the start when you reach the end of the list.

stub(object, 'methodName').returns('hello')
object.methodName() // -> 'hello'

stub(object, 'methodName').returns('many', 'return', 'values')
object.methodName() // -> 'many'
object.methodName() // -> 'return'
object.methodName() // -> 'values'
object.methodName() // -> 'many'

Many JavaScript methods accept callback functions instead of returning a value directly; you can stub this sort of API using the yields modifier. yields takes the argument list that the callback should be called with. As with returns, you can specify multiple argument lists to cycle through.

stub(object, 'methodName').yields(['some', 'args'])

object.methodName(function(a, b) {
    // a == 'some'
    // b == 'args
})

stub(object, 'methodName').yields(['some', 'args'],
                                  ['more', 'data'])

object.methodName(function(a, b) {
    // a == 'some'
    // b == 'args'
})

object.methodName(function(a, b) {
    // a == 'more'
    // b == 'data'
})

Methods stubbed using yields invoke the callback passed to them synchronously, and expect a callback function as the final or the penultimate argument to the method call. This allows a context object to be passed after the callback to specify the binding of this within the callback. If the method is called without a callback in the expected place, an error will be thrown.

Finally, you can specify that the method should throw an error when called; this is done using the raises modifier:

stub(object, 'methodName').raises(new TypeError())

Reacting to input parameters

Often you will want to vary the output of a method based on the arguments it is called with, or check that a method was called with certain arguments. You can do this using the given modifier before a returns, yields or raises modifier. For example we can specify a different return value for each input:

stub(object, 'methodName').given(2,2).returns(4)
stub(object, 'methodName').given(1,2,3).returns(6)

object.methodName(2,2)    // -> 4
object.methodName(1,2,3)  // -> 6

object.methodName()       // -> error

Now our stubbed method will react to the input arguments as specified. If the method receives a call with arguments we haven’t specified, an error is thrown. If you want to just return undefined for all other inputs, just add an unmodified stub(object, 'methodName') to your test setup.

When using yields, given is used to match the arguments before the callback function. For example we could stub jQuery’s Ajax interface like so:

stub(jQuery, 'get').given('/foo.html').yields(['hello'])
stub(jQuery, 'get').given('/bar.html').yields(['world'])

jQuery.get('/foo.html', function(response) {
    // response == 'hello'
})

jQuery.get('/bar.html', function(response) {
    // response == 'world'
})

Argument matchers

Sometimes you don’t know the exact value of the arguments ahead of time, or you only care about a small property of the arguments, like their type or what elements an array contains. For this reason jstest provides a set of matchers that you can use when stubbing to match incoming data. For example, we can set up a stub that reacts to an array containing the string 'test' followed by any number of arguments like this:

stub(object, 'methodName').given(arrayIncluding('test'), anyArgs()).returns(true)

object.methodName(['foo', 'test', 'bar'], 'something')  // -> true
object.methodName(['foo', 'bar'], 'something')          // -> error

The full set of matchers is as follows:

Matchers can be nested, for example this matcher matches an array that contains an object whose foo property is true.

arrayIncluding(objectIncluding({foo: true}))

You can invent your own matchers too. jstest simply invokes matcher.equals(argument) to check if an argument matches the stub when dispatching method calls. You can use any object with an equals() method as a matcher.

Stubbing global objects

Oftentimes you’ll need to stub out a global function or object to make your code unit-testable. To stub globals, just omit the object argument to stub().

// Stubs out alert()
stub('alert').given(instanceOf('string')).returns(undefined)

To stub a global object, just pass in a fake object you want to use in its place. For example, suppose you have some code that relies on jQuery’s Ajax API; you can create a fake object and add stub functions to it. jstest will clean up the stubs you’ve created after each test.

stub('jQuery', {})
stub(jQuery, 'get').given('/foo.html').yields(['foo'])

Stubbing constructors

Constructors are functions that expect to be called using the new keyword for constructing new objects. jstest lets you stub these by passing new as the first argument to stub, followed by the namespace the constructor lives in and its name. For example here’s how you’d stub out JS.Range to return fake objects:

stub('new', JS, 'Range').returns({fake: 'object'})

If the constructor is a global variable, then you can omit the namespace. For example:

stub('new', 'XMLHttpRequest').returns(fakeXHR)

Mock expectations can be applied to constructors just like for any other type of function call.

Mocking methods

Mocking is very similar to stubbing; like stubbing, it replaces methods with fakes, but it also checks those methods are called during the test. If a mocked method is not called with the required arguments, the test fails. To set up a mock expectation for a method call, we use the expect() function instead of stub().

expect(object, 'methodName')

This mock states that object.methodName() should be called at least once with any arguments; if it’s not been called by the end of the test then a test failure will result.

You can use the whole stubbing API shown above when creating mocks, the only difference is that if you use expect() then jstest will alert you if your stubs are not actually called. For example, this code states that a call to jQuery.get('/foo.html', function() { ... }) must be made during the current test, and the given callback will be called with the response 'Hello, World':

expect(jQuery, 'get').given('/foo.html').yielding(['Hello, World'])

(returns, yields and raises are aliased as returning, yielding and raising since these read a little better when setting mock expectations.)

You can specify how many times a given call should be made using the modifiers atLeast, atMost and exactly. These should be added after the given modifier if one is used. For example, this tests that exactly 2 calls to User.create('jcoglan') are made during the test, returning true each time:

expect(User, 'create').given('jcoglan').exactly(2).returning(true)