Strict stubs for better dev experience

Alex Bepple
ITNEXT
Published in
3 min readMay 29, 2018

--

Most modern test-double libraries create stubs with default behavior. Now, if the code under test uses the stub differently, the test will fail late — and even more importantly with an error that might be hard to interpret. Strict stubs help to avoid this problem and thus provide a better experience. testdouble-only-when is a helper for strict stubs for use with testdouble.js.

What’s the problem, again?

Most stubs come with a default behavior. In Java land, Mockito stubs return null, false, etc. when no specific behavior has been rehearsed for the call. In JavaScript land, both testdouble.js stubs and Sinon stubs return undefined, if you call them in unrehearsed fashion.

import td from 'testdouble'
const stub = td.func()
td.when(stub(0)).thenReturn('foo')
assertThat(stub(0), is('foo'))
assertThat(stub(), is(undefined))

This can be even be handy sometimes. Often, however, if you do something with the return value, either process it or pass it on, undefined is likely to cause you problems.

In the following example, the function under test (fut) calls the stub without any params and then calls substring(1) on the return value of the stub. We set up the assertion assertThat(fut(stub), is(‘oo’)) assuming the the stub will return foo. It won’t, however, because, for whatever reason, we rehearsed its behavior differently. So it will return undefined and we end up with an error message that is more confusing than anything else, as it does not point to the root cause of the problem:

TypeError: Cannot read property ‘substring’ of undefined

In executable terms:

describe('Problem', () => {
it('unrehearsed usage fails late because of the consequences of default stub behavior', () => {
const stub = td.function()
td.when(stub(0)).thenReturn('foo')
const fut = (collaborator) => collaborator().substring(1) assertThat(
() => assertThat(fut(stub), is('oo')),
throws(
typedError(TypeError, "Cannot read property 'substring' of undefined")
)
)
})
})

The error you observe is not a consequence of your code under test misbehaving. Rather, the error is a consequence of something entirely different. — Imagine you present with a hot forehead. Did you just run 10k or are you feverish because you have a flu?

Wouldn’t it be great, if the stub told us that it was being used in unexpected fashion?

Enter testdouble-only-when. Let’s rehearse the stub’s behavior using onlyWhen instead of td.when.

import { onlyWhen } from 'testdouble-only-when'
const stub = td.function()
onlyWhen(stub(0)).thenReturn('foo')

Now, using the stub as rehearsed behaves as previously:

assertThat(stub(0), is('foo'))

However, calling the stub differently, e.g. stub() no longer returns undefined. It now results in:

Error: You invoked a test double in an unexpected fashion.
This test double has 1 stubbings and 1 invocations.

Stubbings:
— when called with `(0)`, then return `”foo”`.

Invocations:
— called with `()`.
at …

The test fails early and you know more precisely what to look for.

testdouble-only-when is almost a drop-in replacement for td.when(…). The only exception: multiple stubbings. Because of how testdouble.js stubs are defined, onlyWhen cannot work when you define multiple behaviors. In those rare situations, you can use failOnOtherCalls:

import { failOnOtherCalls } from 'testdouble-only-when'
const stub = td.function()
td.when(stub(0)).thenReturn('foo')
td.when(stub(1)).thenReturn('bar')
failOnOtherCalls(stub)

PS: Some folks might remember that, before Mockito became popular, a major test-double library in Java land was EasyMock. Its stubs fail on unrehearsed usage. One has to explicitly ask an EasyMock stub to be nice in order for it to exhibit default behavior like the one commonly found in other libraries.

If you prefer, you can read a version of this with syntax highlighting as gist/on Roughdraft.

--

--