In test double parlance, to “stub” a method or function is to configure it to return a particular response for a given set of inputs. When used as a noun or referred to as a “stubbing”, a person could be referring to the configured test double itself or the act of returning an artificial response. When the code that you’re testing depends on other units of code to do its job, the (hopefully, vast) majority of those units are invoked with a set of inputs to return some useful output. As a result, when practicing an outside-in test-driven-development workflow, being able to specify that type of interaction in a readable and specific way is very important.
In testdouble.js, we stub behavior with the td.when()
function. In this chapter, we’ll discuss simple stubbings, matching inexact arguments, and advanced configuration options that testdouble.js supports.
All of the examples in this document presume the reader has aliased testdouble
to td
.
The basic structure of a stubbing configuration follows:
var quack = td.function('quack')
td.when(quack()).thenReturn('some return value')
quack() // 'some return value'
As JavaScript APIs go, this should strike readers as unusual, but it’s an intentional design. Note that inside the invocation operator ()
of td.when
that the entire call to the test double is, for lack of a better term, rehearsed. The goal is to demonstrate an invocation of the test double function just as the test expects the subject to invoke the dependency it represents—to a level of precision desired by the situation.
If the above is still bothering you, note that the implementation of testdouble.js doesn’t care what is “passed” into
td.when()
’s first argument position. In fact, what the library will actually do is simply pop the most recent call to any test double off its global call stack wheneverwhen()
is invoked and presume that it was a “rehearsal” invocation for the next stubbing.
The when()
function returns an object containing the function thenReturn
, which is used to specify the value the test double should return if it’s invoked as specified in the when()
call. There are also a thenDo
and thenThrow
response type which are described below.
When practicing outside-in TDD, unit tests are typically completely isolated. As a result, in most cases it’s fair to expect that the test will be able to (and will desire to) specify the exact interactions the subject should have with each of its dependencies. The stubbing API exposed by testdouble.js assumes that the “rehearsed” invocation in a stubbing is made with deeply-equivalents arguments to what the subject is expected to use. (Granted, that is not always desired; later, we’ll show how to use so-called “argument matchers” as a means to loosen that specification.)
The easiest invocation to stub is a no-arg invocation. In fact, you’ve already seen an example!
var quack = td.function('quack')
td.when(quack()).thenReturn('some return value')
quack() // 'some return value'
quack('anything else at all') // undefined
You can also specify arbitrarily many arguments. Given the example above, you might say:
td.when(quack('soft')).thenReturn('quack')
td.when(quack('soft', 2)).thenReturn('quack quack')
td.when(quack('soft', 2, 'hard', 3)).thenReturn('quack quack QUACK QUACK QUACK')
quack('soft') // 'quack'
quack('soft', 2) // 'quack quack'
quack('soft', 2, 'hard', 3) // 'quack quack QUACK QUACK QUACK'
As you can see above, a test double function can be stubbed multiple times for different sets of arguments.
It’s worth noting that stubbing rules are “last-in-wins”, meaning that if multiple stubbings are satisfied by an invocation of the test double, the one configured latest is the one that’s returned. This is almost certainly the behavior you would want in a test, because you could start with a very generic stubbing at the top of a test, and override it later from a point in the test that represents a more specific context.
Sometimes, when all you need from a test double is a one-off stubbing, you’ll want to express both the creation of the test double and that stubbing as tersely as possible. You can do this, since thenReturn
will return the test double instance. That means that:
var woof = td.function()
td.when(woof()).thenReturn('bark')
Is equivalent to this:
var woof = td.when(td.function()()).thenReturn('bark')
Sometimes your subject should invoke a dependency multiple times in the same way. For instance perhaps your subject wants to invoke a randomSound()
function to return 'quack'
, then 'honk'
, then 'moo'
. This can be configured by passing additional arguments to thenReturn()
, like so:
var randomSound = td.function('randomSound')
td.when(randomSound()).thenReturn('quack', 'honk', 'moo')
randomSound() // 'quack'
randomSound() // 'honk'
randomSound() // 'moo'
randomSound() // 'moo'
As you can see, any additional invocations will continue to return the final stubbing in the sequence.
Note that these stubbings will only return their stubbed value if called exactly as they’re demonstrated when td.when()
is invoked. That means, that invoking things like so:
var quack = td.function('quack')
td.when(quack()).thenReturn('some return value')
quack('hi')
quack([1,2,3],4)
quack('anything','at','all')
Will each return undefined
. This differs from the default stubbings one might create with Jasmine or Sinon.js, but it’s by design. When your code calls another function, the arguments it passes probably matter. As a result, testdouble.js defaults to assuming that the inputs passed to stubbed test doubles are necessary.
For instance, if the stubbing were unconditional, we might pat ourselves on the back for writing this test:
function sitInTraffic(horn){
return horn() + '!'
}
var horn = td.function()
td.when(horn()).thenReturn('beep')
assert.equal('beep!', sitInTraffic(horn))
But if our stubbings were unconditional, that means the test would still pass even if we changed the implementation as follows:
function sitInTraffic(horn){
return horn('no really do not honk') + '!'
}
Which would probably not be our intention. As a result, testdouble.js only loosens its rules about whether a stub is said to be “satisfied” when you explicitly indicate that it should.
An “argument matcher” is a special function which, when passed to a test double function during its td.when()
“rehearsal” invocation, can modify the default behavior of whether a particular argument will match a given invocation. (That default behavior, recall, is to expect each actual argument to be deeply equivalent to the rehearsed argument.)
Out of the box, testdouble.js ships with a handful of matchers. They are:
When passed td.matchers.anything()
, any invocation of that test double function will ignore that parameter when determining whether an invocation satisfies the stubbing. For example:
var bark = td.function()
td.when(bark(td.matchers.anything())).thenReturn('woof')
bark(1) // 'woof'
bark('lol') // 'woof'
bark() // undefined
bark(2, 'other stuff') // undefined
When passed td.matchers.isA(someType)
, then invocations of the test double function will satisfy the stubbing when the actual type matches what is passed to isA
. For example:
var eatBiscuit = td.function()
td.when(eatBiscuit(td.matchers.isA(Number))).thenReturn('yum')
eatBiscuit(5) // 'yum'
eatBiscuit('stuff') // undefined
eatBiscuit() // undefined
While Number
is shown above, it will work for any built-in type (e.g. String
or Date
) or any named custom prototypal constructors defined by the user.
When passed td.matchers.contains()
, then a stubbed argument can be loosened to be satisfied by any invocations that simply contain the portion of the argument. This works for several types, including strings, arrays, and objects.
Using contains
on a string argument is pretty straightforward:
var yell = td.function()
td.when(yell(td.matchers.contains('ARGH'))).thenReturn('AYE')
yell('ARGH') // 'AYE'
yell('ARGHHHHHHH') // 'AYE'
yell('ARG') // undefined
Using contains
with regular expressions on string arguments is also supported:
var yell = td.function()
td.when(yell(td.matchers.contains(/ARGH$/i))).thenReturn('AYE')
yell('ARGH') // 'AYE'
yell('ARGHHHHHHH') // 'undefined'
yell('argh') // 'AYE'
yell('ARG') // undefined
Here’s how to use contains
with an array argument:
var jellyBeans = td.function()
td.when(jellyBeans(td.matchers.contains('popcorn', 'apple'))).thenReturn('yum')
jellyBeans(['grape', 'popcorn', 'strawberry', 'apple']) // 'yum'
jellyBeans(['grape', 'popcorn', 'strawberry']) // undefined
You can also use contains
to specify only part of an object. This is especially
useul when a large object is being passed around, but only part of it matters to
the interaction being tested.
var brew = td.function()
td.when(brew(td.matchers.contains({ingredient: 'beans'}))).thenReturn('coffee')
brew({ingredient: 'beans', temperature: 'hot'}) // 'coffee'
brew({ingredient: 'hops', temperature: 'hot'}) // undefined
Better yet, the contains
matcher is capable of doing deep searches of sparse
properties:
var brew = td.function()
td.when(brew(td.matchers.contains({container: {size: 'S'}}))).thenReturn('small coffee')
brew({ingredient: 'beans', container: { type: 'cup', size: 'S'}}) // 'small coffee'
brew({ingredient: 'beans', container: { type: 'cup', size: 'L'}}) // undefined
brew({}) // undefined
If the other built-in matchers don’t serve your needs and you don’t want to roll
your own custom matcher, you can use argThat()
to pass a truth test function
to determine whether an invocation will satisfy the stubbing.
var pet = td.function()
td.when(pet(td.matchers.argThat(function(animals){ return animals.length > 2 }))).thenReturn('goood')
pet(['cat', 'dog', 'horse']) // 'goood'
pet(['cat', 'dog']) // undefined
pet({length: 81}) // 'goood'
If you find yourself needing a matcher that isn’t defined above, you can define your own custom matchers as well.
Keep in mind that any side effects in an argThat
predicate function will be
invoked when you set up subsequent stubbings, so plan accordingly to ensure that
they are free of test-altering side effects and will behave appropriately for
whatever input they might be passed by both the test and the subject.
When you only care that a test double function isn’t passed a certain value,
you can use the not
matcher:
var didSucceed = td.function()
didSucceed(true)
td.verify(didSucceed(td.matchers.not(false)))
Callback APIs are very common, especially in Node.js, and for terseness and convenience sake, testdouble.js provides conveniences for stubbing functions that expect a callback argument.
Suppose you’d like to test-drive a function like this:
function deleteFiles(pattern, glob, rm) {
glob(pattern, function(er, files) {
files.forEach(function(file){
rm(file)
})
})
}
You could write a test for deleteFiles
using td.when
’s thenCallback
API:
var glob = td.function()
var rm = td.function()
td.when(glob('some/pattern/**')).thenCallback(null, ['foo', 'bar'])
deleteFiles('some/pattern/**', glob, rm)
td.verify(rm('foo'))
td.verify(rm('bar'))
The above is very terse and gets the job done, but it makes some assumptions that may not apply to every situation. The above assumes:
glob
’s callback argument is the last argument users will pass to it
(which is a pretty common convention)glob
is not significantNote that all callbacks are invoked synchronously, which makes unit testing functions which are only incidentally asynchronous much simpler.
Take the same example above, but suppose that glob
’s arguments were reversed:
the callback was in the first position and the string pattern was passed second.
This can be done by placing a reference to td.callback
as a marker for its
argument position to the stubbing:
td.when(glob(td.callback, 'some/pattern/**')).thenCallback(null, ['foo', 'bar'])
Suppose in the above example that glob
’s return value was also significant,
in that case we can trade terseness for some added explicitness by treating
td.callback
as an argument matcher and chaining td.when
with thenReturn
as we might normally do:
td.when(glob('some/pattern/**', td.callback(null, ['foo', 'bar']))).thenReturn(8)
Now, calls that satisfy the above stubbing on glob
will both invoke the callback
with the parameters provided to td.callback
and also return 8
.
Some APIs have multiple callback functions, all of which need to be invoked to
properly exercise the subject. Suppose your code depends on
doWork(onStart, onEnd)
, and both callback arguments should be invoked in a
single test, you could:
var doWork = td.function()
td.when(doWork(td.callback(null, 42), td.callback(null, 58))).thenReturn()
var percent = 0
doWork(function(er, progress) {
percent += progress
}, function(er, progress) {
percent += progress
})
assert.equal(percent, 100)
Remember, not every test needs to invoke every callback; it’s fine to specify
them separately, especially if it would lead to more focused tests. You could
break up the above into two tests by matching the other function argument with
an isA(Function)
matcher:
td.when(doWork(td.callback(null, 42), td.matchers.isA(Function))).thenReturn()
In addition to thenReturn
, you can stub a method to throw an exception when
invoked in a certain way.
var save = td.function()
td.when(save('bob')).thenThrow(new Error('Name taken'))
save('bob') // throws error 'Name taken'
As a little touch of shorthand, you can stub a method to return an immediately resolved or rejected promise.
To stub a resolved Promise, use thenResolve
:
var fetch = td.function()
td.when(fetch('/user')).thenResolve('Jane')
fetch('/user').then(function (value) {
console.log(value) // prints "Jane"
})
To stub a rejected Promise, use thenReject
:
var fetch = td.function()
td.when(fetch('/user')).thenReject('Joe')
fetch('/user').catch(function (value) {
console.log(value) // prints "Joe"
})
Note that while the testdouble.js API is itself entirely synchronous, most
Promise implementations will ensure that then
is invoked on a subsequent tick
of the event loop, which will in turn require any tests that use Promise objects
to run asynchronously.
If your runtime doesn’t support native promises, or if your application depends on a particular promise library, you’ll first need to point testdouble.js to it. This only needs to be done once (perhaps in a global test helper).
td.config({
promiseConstructor: require('bluebird')
})
For more on configuration, read the appendix on td.config.
This shouldn’t be needed very frequently, but when a depended-on method needs to
have a side effect, you can stub that side effect by passing a function to
thenDo
in a stubbing.
For instance, if you were testing a function like this:
function numbers(upTo, append) {
for(var i=0; i < upTo; i++) {
append(i)
}
}
You could design a test to ensure the provided append
function is invoked as
you’d expect by simulating the side effect in the test using thenDo
:
var items = []
var append = td.function()
td.when(append(td.matchers.anything())).thenDo(function(x){ items.push(x) })
numbers(5, append)
assert.deepEqual(items, [0,1,2,3,4])
Note that the above verification could also be done with td.verify
, but might
be more straightforward in some situations.
So far, we’ve seen td.when()
only invoked with one argument, but it sports a
second configuration parameter as well, with a handful of options.
Keep in mind that these additional options are made available because they each have occassional genuine value, but they ought to only be needed sparingly. If you find yourself reaching for any of these options on a very frequent basis, we recommend you pause and ask what about the design of your code is encouraging the perceived need of that option.
Sometimes, a subject will call a dependency with arbitrarily many arguments, but
for the purpose of a test, only the earlier arguments are significant to the
interaction being considered “correct”. Once in a great while, none of the
arguments passed to a depended-on function matter at all. In either of these
cases, you can use the ignoreExtraArgs
configuration property when stubbing
with when()
Here’s an example:
logger = td.function()
td.when(logger("Outcomes are:"), {ignoreExtraArgs: true}).thenReturn('loggy')
logger("Outcomes are:") // 'loggy'
logger("Outcomes are:", "stuff") // 'loggy'
logger("Outcomes are:", "stuff", "that", "keeps", "going") // 'loggy'
logger("Outcomes are not:", "stuff") // undefined
Or, in the case where literally none of the arguments matter:
whatever = td.function()
td.when(whatever(), {ignoreExtraArgs: true}).thenReturn('yesss')
whatever() // 'yesss'
whatever(1,2,3,4,5) // 'yesss'
Sometimes you want to ensure that a function will only return a particular value
at most n
times.
Note that for simple cases, one could use
sequential stubbing like
td.when(...).thenReturn('foo','foo',undefined)
to return 'foo'
at most two
times.
For more complex cases, the times
property can be configured to effectively
disable a stubbing after it’s been satisfied n
times. This is especially
useful in cases where a generic stubbing is configured first, but a more specific
stubbing is configured later and you want to fall back on the earlier generic
stubbing later. If this sounds incredibly obtuse and convoluted, you’d be right
to think so.
Here’s an example of using the times
property for a complex stubbing
arrangement:
var nextToken = td.function()
td.when(nextToken(td.matchers.isA(Number))).thenReturn("foo")
td.when(nextToken(3), {times: 2}).thenReturn("bar")
nextToken(3) // 'bar'
nextToken(5) // 'foo'
nextToken(3) // 'bar'
nextToken(3) // 'foo'
[Note: you probably don’t need this option. Using it everywhere smells of overly defensive specifications.]
By default, callback stubbings (whether configured via the td.callback
matcher
or by invoking thenCallback
) are invoked synchronously. Since testdouble.js is
designed for isolated unit tests, this is usually what you want, because
comprehending and troubleshooting synchronous test scripts will always be much
simpler than asynchronous ones. However, in the event that you want to ensure
the subject isn’t inadvertently relying on this synchronous execution of
callbacks, you can ensure those callbacks are scheduled to a later execution
stack by setting the defer
option to true
.
Take this example of an erroneously passing test:
// Subject under test
function printBalance (id, fetchBalance, print) {
var balance;
fetchBalance(id, function (er, amount) {
balance = amount
})
print('Your balance is ' + balance)
}
// Test body
var fetchBalance = td.function('.fetchBalance')
var print = td.function('.print')
td.when(fetchBalance(42)).thenCallback(null, 1337)
printBalance(42, fetchBalance, print)
td.verify(print('Your balance is 1337'))
The above passes, but suppose that in practice fetchBalance
is going to
invoke the callback asynchronously—if that’s the case, then this passing test
will be lying to us! To guard against this category of test smells, you can set
the defer
option when stubbing the async dependency, like so:
td.when(fetchBalance(42), {defer: true}).thenCallback(null, 1337)
Now the test above will fail—shiny! Keep in mind that you’ll have to make the
overall test asynchronous (e.g. a done
callback in Mocha/Jasmine) when using
the defer
option.
[Note: while this option is also supported for the Promise stubbings
thenResolve
and thenReject
, all standard Promise implementations will
already ensure your event handlers will fire asynchronously on a later call
stack.]
[Note: you really probably don’t need this. You might need defer
above, but
only reach for this delay
option when your subject’s behavior depends on the
order in which various async operations are completed.]
When using td.callback
, thenCallback
, thenResolve
, or thenReject
, you
can use the delay
option to wait a set number of milliseconds before the
operation completes.
Here’s a quick and silly example of what this option entails:
var fetch td.function('.fetch')
td.when(fetch('/A'), {delay: 20}).thenCallback(null, 1)
td.when(fetch('/B'), {delay: 10}).thenCallback(null, 2)
td.when(fetch('/C'), {delay: 5}).thenResolve(3)
fetch('/A', function (er, result) {}) // will be invoked 3rd
fetch('/B', function (er, result) {}) // will be invoked 2nd
fetch('/C').then(function (result) {}) // will be invoked 1st
Every now and then, the code under test will mutate the object you initially pass into a stubbing configuration, and you want to be sure that when the test double function is invoked by your code, the stubbing is determined to be satisfied (or not) by comparing the actual call against the value at the time you configured it.
Confused yet? Here’s a quick example of how testdouble.js behaves by default:
const func = td.func()
const person = { age: 17 }
td.when(func(person)).thenReturn('minor')
// Later, in your code
person.age = 30
func(person) // 'minor'
Maybe you don’t want this! Maybe you want to be sure the stubbing is only
satisfied so long as the arguments are exactly as they were when you configured
the stubbing. You can do that! By setting cloneArgs
to true, you can do the
following:
const func = td.func()
const person = { age: 17 }
td.when(func(person), { cloneArgs: true }).thenReturn('minor')
// Later, in your code
person.age = 30
func(person) // undefined
While passing in data with an expectation that it be mutated by your subject is generally Not Recommended™, this option should enable edge cases where mutation is unavoidable or out of your control.
And that’s about all there is to say about stubbing. Great news, because the
other major feature of any test double library—verifying an invocation took
place—has an API that was carefully-designed to be completely symmetrical! Read
on about verifying interactions with verify()
,
and rest easy knowing that you already know exactly how to do it.
Previous: Creating Test Doubles Next: Verifying interactions