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:
anything()
matches any single valueanyArgs()
matches any number of values (including none) at the end of the argument listinstanceOf(type)
matches any value of the given type, e.g.instanceOf('string')
orinstanceOf(JS.SortedSet)
arrayIncluding(value[, value2, ...])
matches anArray
containing all the given valuesobjectIncluding({key: value[, key2: value2, ...]})
matches anObject
containing all the given key-value pairs.match(pattern)
matches any value thatpattern
matches.pattern
can be aRegExp
or any object that responds tomatch()
, like aModule
or aRange
.
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)