If you haven’t read through the document on stubbing, be sure to read it first. Why?
First, because the API for verifying invocations of test double functions is essentially identical to the API for stubbing responses, so if you’re familiar with how to stub using testdouble.js, there isn’t much more to know about verifying interactions.
Second, verifying that a method was invoked is only necessary when a depended-on function is being invoked for its side effect (as opposed to returning a meaningful value). For starters, pure functions that return useful values without a side effect are much easier to understand, test, and maintain, so we’d be better off writing more of them—that means it’d be a bit worrisome to see a test suite with lots of test double verification calls.
One last word of warning, never verify an invocation that was also stubbed. If the stubbing is necessary for the test to pass, then adding a verification for the same invocation is redundant and unnecessary. This is a counter-intuitive point for a lot of people, so we’ll just leave it at, “only verify an invocation when there’s no other way to assert that your subject is doing what you want it to do.”
OK, disclaimers aside, lots of functions have side effects by design, and
testdouble.js provides a verify()
function for asserting that an invocation
happened exactly as you expected it. Here’s how to use it.
The examples in this document assume you’ve aliased testdouble
to td
.
A basic verification looks like this:
var quack = td.function('quack')
quack('QUACK')
td.verify(quack('QUACK')) // Nothing happens, because verification was satisfied
As you can see, td.verify
is very similar to td.when
, in that it ignores the
first argument passed to it so that in our test we can write a “demonstration”
of how we expected the test double to have been invoked by our code under test.
When a verification fails, an error is thrown with a message like the following:
td.verify(quack())
Error: Unsatisfied verification on test double `quack`.
Wanted:
- called with `()`.
But was actually called:
- called with `("QUACK")`.
at Object.module.exports [as verify] (/Users/justin/code/testdouble/testdouble.js/lib/verify.js:22:15)
As you can see, the expected arguments of the failed verification are printed along with any actual invocations of the test double function.
All of testdouble.js’s rules about argument precision when stubbing apply here, too. By default, each expected argument is tested against the arguments actually passed to the test double with lodash’s _.isEqual function.
var enroll = td.function()
enroll({name: 'Joe', age: 22, gender: null})
td.verify(enroll({name: 'Joe', age: 22, gender: null})) // passes — deeply equal
td.verify(enroll({name: 'Joe', age: 22})) // throws - missing property
td.verify(enroll({name: 'Joe', age: 23, gender: null})) // throws - not equal
Each of the argument matchers supported when stubbing also work when verifying an interaction. Below are simple examples of each built-in matcher
The anything()
matcher will only ensure that an argument was passed, but will
ignore whatever its value was.
var bark = td.function()
bark('woof')
td.verify(bark('woof')) // passes
td.verify(bark(td.matchers.anything())) // passes
td.verify(bark(td.matchers.anything(), td.matchers.anything())) // throws - was 1 arg
td.verify(bark()) // throws - 1 arg needed
The isA()
matcher can be used to verify a matching type for a given argument.
var eatBiscuit = td.function()
eatBiscuit(44)
td.verify(eatBiscuit(44)) // passes
td.verify(eatBiscuit(td.matchers.isA(Number))) // passes
td.verify(eatBiscuit(td.matchers.isA(Date))) // throws - 44 is not a Date
td.verify(eatBiscuit(td.matchers.isA(Object))) // throws - Number is not an Object
Unfortunately, the error message generated when a verification fails due to an argument matcher mis-match is not very informative. If you’d like to help out, see this issue to improve the argument matcher API.
The contains matcher is satisified if the passed-in portion of a string, array, or object is found on an actual invocation of the test double.
var log = td.function()
log('Why hello there!')
td.verify(log('Why hello there!')) // passes
td.verify(log(td.matchers.contains('hello'))) // passes
td.verify(log(td.matchers.contains('goodbye'))) // throws - string not found
var join = td.function()
join(['this','and','that'])
td.verify(join(['this','and','that'])) // passes
td.verify(join(td.matchers.contains('and'))) // passes
td.verify(join(td.matchers.contains('this','that'))) // passes
td.verify(join(td.matchers.contains('this','not that'))) // throws - 'not that' absent
var brew = td.function()
brew({ingredient: 'beans', temperature: 'cold'})
td.verify(brew({ingredient: 'beans', temperature: 'cold'})) // passes
td.verify(brew(td.matchers.contains({ingredient: 'beans'}))) // passes
td.verify(brew(td.matchers.contains({temperature: 'hot'}))) // throws - wa cold
And, just like when stubbing, contains()
can be used to match deeply-nested
object properties.
When the argument match needed is more complex than can be described above, one
option is to pass a truth test to argThat()
, like so:
var pet = td.function()
pet(['cat', 'dog'])
td.verify(pet(td.matchers.argThat(function(n){ return n.length > 1 }))) // passes
td.verify(pet(td.matchers.argThat(function(n){ return n.length > 2 }))) // throws
Remember that if none of the matchers above suit you, writing your own is as
easy as writing a function that returns an object with a __matches
property.
Read the document on custom matchers for more information.
Often in JavaScript, we’ll pass an anonymous or privately-scoped function from our subject under test to one of its dependencies. In order to fully test the interaction between the subject and such a dependency, we need a way to get a reference to that function.
One way to do this is to make the function publicly reachable and put it under direct test. That has the benefit of being simple to read and explicit, but often comes at the added cost of sacrificing the convenience of lexically-scoped values and at the risk of cluttering an API with highly contextual one-off bits of behavior.
Another way to accomplish the same thing is with what is called an “argument captor”. You can think of an argument captor as a special type of argument matcher. To be more precise, an argument captor is an object that generates an argument matcher which always reports a successful match, all-the-while storing the value passed into said matcher for later access by the originating test.
But, that sounds confusing! Let’s see an example:
Let’s say that we wrote a test for a function that looked like this:
function logInvalidComments(fetcher, logger) {
fetcher('/comments', function(response){
response.comments.forEach(function(comment) {
if(!comment.valid) {
logger('Hey, '+comment.text+' is invalid')
}
})
})
}
JavaScript has a knack for enabling very dense functions—the above makes an HTTP
request, handles the response, and for each invalid comment
resource, writes
out a particular logger statement. So, how do we verify that the logger()
function is being invoked exactly as we specified?
You could use an argument captor to write a sort of two-staged test for both the top-level function along with its embedded anonymous function.
The test begins similarly to what we’ve seen before, with a verification of the
top-most depended-on function, fetcher
:
var logger = td.function('logger'),
fetcher = td.function('fetcher'),
captor = td.matchers.captor()
logInvalidComments(fetcher, logger)
td.verify(fetcher('/comments', captor.capture()))
The only novel thing seen above is the invocation of captor()
to create a new
argument captor object and its use in the verify()
demonstration call to
fetcher
with captor.capture()
. Remember, we’re said to be “capturing” the
value of that second argument because there’s no other way for our test to get
a reference to that function without changing the production source code.
Now that we’ve captured a reference to the anonymous callback function, we can
put it under test, too. Once capture()
is called, the captor
object will
retain the captured argument on a property named value
:
var response = {comments: [{valid: true}, {valid: false, text: 'PANTS'}]}
captor.value(response)
td.verify(logger('Hey, PANTS is invalid'))
This style is definitely verbose, but it’s very explicit and entirely synchronous. Rather than write asynchronous unit tests of asynchronous code, this pattern enables developers to maintain control over how their code executes by testing it synchronously. The benefits to this are comprehensability of what the test does at runtime, easier debugging, and no reliance on a test framework to provide async support.
Is writing tests in this style worth it? A better question might be, “is there an easier-to-use design conducive to an outside-in TDD workflow?” Without casting judgment on passing around anonymous functions per se, I’ve found that they’re typically best used in two places, neither of whose tests do I use test doubles:
It’s due to the reasoning above that one should question the frequent use of argument captors or the perceived need for asynchronous behavior in unit tests. For related conversation, check out Gary Bernhardt’s excellent talk on this topic called Boundaries.
In some cases you may want to capture multiple invocations of the same function or
method in one test. A common usecase for this is subscription based APIs where a
callback will be invoked for each message. To handle this usecase, captors
expose
a values
array which will hold each argument passed during every invocation of the
callback:
var captor = td.matchers.captor(),
responseCallback = td.function();
subscribe('/chat', responseCallback); // subscribe() will call responseCallback twice
td.verify(responseCallback('/chat', captor.capture()))
assert.equal(captor.values[0], 'first message');
assert.equal(captor.values[1], 'second message');
Verifications can be configured in the exact same ways that stubbings
can. By passing an options object
as the second argument to verify()
, you can modify the behavior of an assertion.
For added clarity, below are some example uses to demonstrate their behavior.
When you don’t care about any of the args passed to a function, or only the first
n arguments passed, you can use the ignoreExtraArgs: true
option:
var print = td.function()
print('some', 'stuff', 'out', 'like', 8)
td.verify(print()) // throws, missng all arguments
td.verify(print(), {ignoreExtraArgs: true}) // passes
td.verify(print('some'), {ignoreExtraArgs: true}) // passes
td.verify(print('some', 'stuff'), {ignoreExtraArgs: true}) // passes
td.verify(print('some', 'stuff', 'NOPE'), {ignoreExtraArgs: true}) // throws, wrong arg
If you’d like to improve the error message when ignoreExtraArgs
is used,
consider contributing a pull request for this
issue.
Sometimes, we want to verify that a test double was called an exact number of
times in a certain way. With the times
option we can do that.
var save = td.function()
save('thing')
save('thing')
td.verify(save('thing')) // passes
td.verify(save('thing'), {times: 1}) // throws - was called twice
As a silly example to combine both options so far, consider this test to ensure that a function was never called, regardless of arguments:
var doNotCall = td.function()
td.verify(doNotCall(), {times: 0, ignoreExtraArgs: true}) // passes
What if you want to verify a call took place and the subject (for better or worse) mutated an argument after it was passed to the test double function? Since testdouble.js saves arguments by reference by default, you won’t get the result you want:
const func = td.func()
const person = { age: 17 }
// later, in your code
func(person)
person.age = 30
// back in your test
td.verify(func({ age: 17 })) // 💥 Test failure! td.js recorded age as 30!
For cases like these, you can work around the mutation by setting cloneArgs
to
true
:
const func = td.func()
const person = { age: 17 }
// later, in your code
func(person)
person.age = 30
// back in your test
td.verify(func({ age: 17 }), { cloneArgs: true }) // 😌 all good
And that’s everything there is to know about verifying behavior with testdouble.js! At this point, you know everything you need to know to be pretty dangerous writing isolated tests.
Previous: Stubbing behavior Next: Replacing Real Dependencies with Test Doubles