Getting Started

To define a unit test with spooning, pass a name and a function as parameters to spooning.test. The name usually takes the form of “Should do the thing” and the function should accept a callback and implement the following behavior:

  • to indicate a failure, callback with an error as the first parameter
  • to indicate a pass, callback with a “falsy” (false|0|undefined|null) value as the first parameter

This should be instantly familiar to those who have used callbacks before; it is the standard convention.

Two additional wrapper functions (testSync and testPromise) are provided to make it just as easy to define synchronous tests and Promise-based tests.

In keeping with the principle that tests should look like the code they are testing, there is no special syntax unique to spooning tests.

Installing Spooning

Install spooning as a dev dependency of your project:

npm install -D spooning

Requires node 6 or later.

Creating Your First Test

Create a file named hello.test.js with the following contents:

const {test, run, exit} = require('spooning');

test('Should say hello', (callback) => {
    setTimeout(() => {
        callback(null, 'Hello!');
    }, 100);
});

run(exit);

Congratulations! You just implemented your first unit test with spooning. It doesn’t actually check anything, but it does display a pleasant diagnostic message.

For simplicity’s sake, this example combines the code that runs the test with the code that defines the test. This is almost never practical for a real project. See Running Tests for examples of how to organize and run tests across multiple files with or without the CLI.

Running Your First Test

node hello.test.js

This should produce the following output:

1..1
ok 1 - Should say hello
# Hello!
# test: 1
# pass: 1
# fail: 0

Wrapper Functions

The testSync and testPromise functions provide a way to define synchronous and Promise-based tests, respectively.

testPromise

Use testPromise to define the test as a function that returns a Promise object:

const {testPromise} = require('spooning');

testPromise('Should pass (promise)', () => new Promise((resolve) => {
    resolve('optional diagnostic message');
}));

testPromise('Should fail (promise)', () => new Promise((resolve, reject) => {
    reject(new Error('failed'));
}));

testSync

The testSync function is provided for defining synchronous tests:

const {testSync} = require('spooning');

testSync('Should pass (sync)', () => {
    return 'optional diagnostic message';
});

testSync('Should fail (sync)', () => {
    throw new Error('failed');
});

Using Assertions

Here’s an example using the native assert library provided by node:

const {ok, strictEqual} = require('assert');

testSync('Should pass', () => {
   ok(true);
});

testSync('Should fail', () => {
   strictEqual('A', 'B');
});

You can also use assertions (and/or throw Errors) in the root scope of async tests, but otherwise errors have to be caught in the scope where they are thrown:

test('Should fail with assert (root scope)', (callback) => {
    ok(false, 'failed');
    callback();
});

test('Should fail with assert (other scope)', (callback) => {
    setTimeout(() => {
        try {
            ok(false, 'failed');
            callback();
        }
        catch(e) {
            callback(e);
        }
    }, 100);
});

Optional Callback

An optional callback may be provided to any of the test definition functions. The callback will be called with an error if the test failed (null if it passed) and a TestResultInfo object containing the test information.

test('Should pass', (callback) => {

    setTimeout(() => {
        callback(null, 'optional diagnostic message');
    }, 100);
    
}, (error, info) => {    
    // error === null
    // info === { idx: 1, name: 'Should pass', diagnosticMessage: 'optional diagnostic message', error: null }
});

Returning a Promise

Promise purists should note that testPromise does not automatically return a Promise. Returning a Promise that resolves/rejects when the test passes/fails is supported, but it is opt-in to avoid “unhandled promise rejection” warnings. Use promisify (available in the native util library provided by node) to wrap any of the test function calls. info is a TestResultInfo object.

const {promisify} = require('util');
const vow = promisify(testPromise);

vow('Should pass', () => new Promise((resolve) => {
    resolve('optional diagnostic message');
})).then((info) => {
    // info === { idx: 1, name: 'Should pass', diagnosticMessage: 'optional diagnostic message', error: null }
});

vow('Should fail', () => new Promise((resolve, reject) => {
    reject(new Error('error message'));
})).catch((error) => {        
    // handle error
});

Observable Events

spooning emits events throughout the course of running the tests. See the API Documentation for more information.

const spooning = require('spooning');
spooning.on('runEnd', ({passed, total}) => {
    // passed: count of tests that passed
    // total: count of tests that ran
}).on('error', (error) => {
    // handle error
});

Next Chapter

Continue to Writing Tests for more practical examples.