Building Your Own (JavaScript) Test Doubles

A test double is an object or function that behaves like a real piece of code, but is, instead, solely for a test. It mimics the functionality of something, without being that something.

There are plenty of frameworks to deal with test doubles in JavaScript (like sinon), but it’s helpful to the various types of test doubles by building your own. This is also a vialble alternative to frameworks for smaller projects: it’s often easier to understand a simple anonymous function as a test double than learning an entire framework.

We’re going to create various test doubles for a function that interacts with an API. The system under test is just another function that takes a function as an argument.

const fetch = require('isomorphic-fetch');

function httpbinUuid() {
    return fetch('https://httpbin.org/uuid')
        .then(resp => resp.json())
        .then(json => json.uuid);
}

function systemUnderTest(uuid) {
    return uuid().then(uuid => {
        return `New UUID is ${uuid}`;
    });
}

The language here is JavaScript, but the same ideas can be applied to any language. For the sake of simplicity, no testing framework is used below. We just check for the main module and run the test.

The code for this post is available on github.

What are We Doubling

What’s the interface we’re trying to create test doubles for? The httpbinUuid function above. It takes no arguments and returns a promise that resolves to a string. That’s the interface.

function uuid(): Promise {
    // ...
}

Fakes

Fakes are the easiest doubles to understand: they are simplified implementations of real objects. In our example above a fake function may just resolve to a string given its constructor.

const assert = require('assert');
const subject = require('./subject');

function fakeUuid(retval) {
    return function () {
        return Promise.resolve(retval);
    };
}

if (require.main === module) {
    subject.systemUnderTest(fakeUuid('testing123')).then(result => {
        assert.strictEqual(result, 'New UUID is testing123');
    })
}

Rather than interact with an API to generate a “uuid” we instead use an in memory value. fakeUuid takes a string argument and then returns a function that resolve to that string argument.

Remember, that’s the interface we’re doubling: a function that takes no arguments and returns a promise which resolves to a string.

Notice that the fake still has some logic as it takes a value that it returns — it has a constructor.

Stubs

Stubs, on the other hand, do not have logic. They return pre-programmed values and provide a way to pass a system under test input indirectly.

In JavaScript, it’d be more common to use anonymous functions for stubs unless the stub would need to be re-used more than once.

const assert = require('assert');
const subject = require('./subject');

function stubUuid() {
    return Promise.resolve('testing123');
}

if (require.main === module) {
    subject.systemUnderTest(stubUuid).then(result => {
        assert.strictEqual(result, 'New UUID is testing123');
    })
}

The stub in this case just returns the promise directly. No real logic, it just returns a canned response.

Spies

A test spy records its calls for later inspection.

In our example above we know the uuid function is called because systemUnderTest wouldn’t work if it wasn’t. In more complex systems, however, one might want to verify that a function was called and maybe inspect its arguments.

const assert = require('assert');
const subject = require('./subject');

function spyUuid(retval) {
    let spy = {
        calls: 0,
        args: [],
    };
    spy.uuid = function (...args) {
        this.calls += 1;
        this.args.push(args);
        return Promise.resolve(retval)
    }.bind(spy);

    return spy;
}


if (require.main === module) {
    const spy = spyUuid('testing123');
    subject.systemUnderTest(spy.uuid).then(result => {
        assert.strictEqual(result, 'New UUID is testing123');
        assert.strictEqual(spy.calls, 1, 'should have been called once');
        assert.deepEqual(spy.args[0], [], 'should not have been called with any arguments');
    })
}

The above takes our test doubles to the next level. It verifies that we did actuall call the uuid function as well as makes sure that our systemUnderTest called it correctly without any arguments.

Mocks

What makes a spy a spy is tha the verification is done after the fact. A mock, on the other hand, has pre-defined verification.

Mocking frameworks, therefore, often have APIs that require a setup step before things are executed.

const m = mock(/* ... */);
m.expects('someMethod').once().returns('a value');
// etc

It’s not really practical to build an API like that ourselves, so our simplified mocks will simply have pre-programmed verifications that happen when the function is called.

const assert = require('assert');
const subject = require('./subject');

function mockUuid(...args) {
    assert.deepEqual(args, [], 'should not be called with any arguments');
    return Promise.resolve('testing123')
}


if (require.main === module) {
    subject.systemUnderTest(mockUuid).then(result => {
        assert.strictEqual(result, 'New UUID is testing123');
    })
}

The verification step, making sure no arguments were passed, is pre-programmed in the mockUuid function. If something were to change and systemUnderTest started passing arguments to its uuid function the test would fail right when it was called.

Which Type of Test Double is Best?

Whichever test double makes you feel warm and fuzzy about shitting the final product. In most cases that means a spy or a mock where verification of arguments and times called can happen. Fakes and stubs have their place; I really like using fakes for things like loggers or other simple, fire-and-forget type dependencies.