Skip to content

Latest commit

 

History

History
414 lines (299 loc) · 14.8 KB

6-verifying-invocations.md

File metadata and controls

414 lines (299 loc) · 14.8 KB

Verifying interactions

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.

td.verify()

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.

Arguments

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

Relaxing verifications with argument matchers

Each of the argument matchers supported when stubbing also work when verifying an interaction. Below are simple examples of each built-in matcher

td.matchers.anything()

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

td.matchers.isA()

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.

td.matchers.contains()

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.

Strings
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
Arrays
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
Objects
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.

td.matchers.argThat()

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

custom argument matchers

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.

Multi-phase assertions with argument captors

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:

td.matchers.captor()

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:

  • Calls to asynchronous I/O - one reason for needing to pass a function is to defer some bit of evaluation until an I/O operation has completed. I do my best to draw my I/O interactions near enough to the entry point such that my "domain logic" is relatively unconcerned with it. If I succeed at making the entry point mostly branchless and imperative, I'll test it only via an integration test and not concern myself with trying to unit test it with test doubles
  • Transforming data - the other major use for passing around anonymous functions is when using a library like lodash to translate some initial value using operations like map, reduce, zip, and groupBy. I tend not to use test doubles in tests of functions that do much of this either, because they can usually be expressed or composed into pure functions that can be tested perfectly well without any dependencies to be isolated from.

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.

Capturing multiple invocations with td.matchers.captor()

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');

Configuring verifications

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.

ignoreExtraArgs

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.

times

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
Never, ever call something

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

cloneArgs

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

Congratulations!

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