node-async-testing

Simple, intuitive testing for node.js

Writing Test Suites

The hard part of writing a test suite for asynchronous code is figuring out when a test is done, and feeling confident that all the code was run.

node-async-testing addresses that by...

  1. giving each test its own unique assert object. This way you know which assertions correspond to which tests.
  2. allowing you to run the tests one at a time. This way it is possible to add a global exception handler to the process and know exactly which test cause an error.
  3. requiring you to tell the test runner when the test is finished. This way you don't have any doubt as to whether or not an asynchronous test still has code to run.
  4. allowing you to declare how many assertions should take place in a test. This way you can ensure that your callbacks aren't being called too many or too few times.

All of the examples in this page can be seen in the test/test-overview.js suite and can be executed by running the following command from within the node-async-testing directory:

node test/test-overview.js

Page outline:

Tests

node-async-testing tests are just a functions that take a ‘test object’:

function asynchronousTest(test) {
  setTimeout(function() {
    // make an assertion (these are just regular Node assertions)
    test.ok(true);

    // finish the test
    test.finish();
  });
}

The test object is where all the action takes place. You make your assertions using this object (test.ok(), test.deepEquals(), etc) and use it to finish the test (test.finish()). Basically, all the actions that are directly related to a test use this object.

Test objects have the following properties:

test.finish()

node-async-testing assumes all tests are asynchronous. So in order for it to know that a given test has completed, you have to ‘finish’ the test by calling test.finish(). This let's the test runner know that that particular test doesn't have any more asynchronous code running.

Even if a test is not asynchronous you still have to finish it:

    function synchronousTest(test) {
      test.ok(true);
      test.finish();
    }

Tip:

It is important that no code from this test is ran after this function is called, otherwise, node-async-testing will think that code corresponds to a different test. Be careful!

If test.finish() is called more than once, or an assertion is made after finish() has already been called, an error will be thrown. These features help catch test errors. Use these to your advantage. For example, at the beginning of an asynchronous callback make an assertion! If the test has already finished (the callback is being called when it isn't supposed to be) then the assertion will catch that the test has already finished and prevent the callback from running any code that can interfere with other tests. Example:

module.exports =
  { 'okay': function(test) {
      setTimeout(function() {
        /* run code */
        test.finish();
      }
    }
  , 'GOOD PRACTICE!': function(test) {
      setTimeout(function() {
        test.ok(true); // make sure test isn't already finished
        
        /* run code */

        test.finish();
      }
    }
  };
test.numAssertions

node-async-testing lets you be explicit about the number of assertions run in a given test: set numAssertions on the test object. This can be very helpful in asynchronous tests where you want to be sure all callbacks get fired:

    suite['test assertions expected'] = function(test) {
      test.numAssertions = 1;

      test.ok(true);
      test.finish();
    }

If you are testing asynchronous code, I highly recommend using test.numAssertions. node-async-testing depends on test.finish() to tell it when a test is finished, so if code is run after finish() was called you will get innacurate test results. Use numAssertions to be sure all your callbacks are called and that they aren't called too many times.

test.uncaughtExceptionHandler

node-async-testing lets you deal with uncaught errors. If you expect an error to be thrown asynchronously in your code somewhere (this is not good practice, but sometimes when using other people's code you have no choice. Or maybe it is what you want to happen, who am I to judge?), you can set an uncaughtExceptionHandler on the test object:

    suite['test catch async error'] = function(test) {
      var e = new Error();

      test.uncaughtExceptionHandler = function(err) {
        test.equal(e, err);
        test.finish();
      }

      setTimeout(function() {
          throw e;
        }, 500);
    };

This property can only be set when running suites serially, because otherwise node-async-testing wouldn't know for which test it was catching the error.

Assertions

Additionally, the test object has all of the assertion functions that come with the assert module bundled with Node.

These methods are bound to the test object so node-async-testing can know which assertions correspond to which tests.

test.ok(value, [message])
Tests if value is a true value, it is equivalent to assert.equal(true, value, message);.
test.equal(actual, expected, [message])
Tests shallow, coercive equality with the equal comparison operator ( == ).
test.notEqual(actual, expected, [message])
Tests shallow, coercive non-equality with the not equal comparison operator ( != ).
test.deepEqual(actual, expected, [message])
Tests for deep equality.
test.notDeepEqual(actual, expected, [message])
Tests for any deep inequality.
test.strictEqual(actual, expected, [message])
Tests strict equality, as determined by the strict equality operator ( === )
test.notStrictEqual(actual, expected, [message])
Tests strict non-equality, as determined by the strict not equal operator ( !== )
test.throws(block, [error], [message])
Expects block to throw an error.
test.doesNotThrow(block, [error], [message])
Expects block not to throw an error.
test.ifError(value, [message])
Tests if value is not a false value, throws if it is a true value. Useful when testing the first argument, error in callbacks.

Custom assertions

Because node-async-testing needs to bind the assertion methods to the test object, you can't just use any assertion you have lying around. You have to first register it:

var async_testing = require('async_testing');
async_testing.registerAssertion('customAssertion', function() { ... });

exports['test assert'] = function(test) {
  test.customAssertion();
  test.finish();
}

Check out test/test-custom_assertions.js for a working example.

Suites

node-async-testing is written for running suites of tests, not individual tests. A test suite is just an object with test functions:

var suite = {
  'asynchronous test': function(test) {
    setTimeout(function() {
      test.ok(true);
      test.finish();
    });
  },
  'synchronous test': function(test) {
    test.ok(true);
    test.finish();
  }
}

Sub-suites

node-async-testing allows you to namespace your tests by putting them in a sub-suite:

var suite =
  { 'namespace 1':
    { 'test A': function(test) { ... }
    , 'test B': function(test) { ... }
    }
  , 'namespace 2':
    { 'test A': function(test) { ... }
    , 'test B': function(test) { ... }
    }
  , 'namespace 3':
    { 'test A': function(test) { ... }
    , 'test B': function(test) { ... }
    }
  };

Suites can be nested arbitrarily deep:

var suite =
  { 'namespace 1':
    { 'namespace 2':
      { 'namespace 3':
        { 'test A': function(test) { ... }
        , 'test B': function(test) { ... }
        , 'test C': function(test) { ... }
        }
      }
    }
  };

wrap()

node-async-testing comes with a convenience function for wrapping all tests in an object with setup/teardown functions. This function is called wrap and it takes one argument which is an object which can have the following properties:

suite
This property is required and should be the suite object you want to wrap.
setup(test, done)
This function is run before every single test in the suite. The first argument is the test object for which this setup is being run. If you want to pass additional data/objects to the test, you should set them on the test object directly. Because setup might need to be asynchronous, you have to call the second argument, done(), when it is finished. Optional.
teardown(test, done)
This function is run after every single test in the suite. teardown functions are called regardless of whether or not the test succeeds or fails. It gets the same arguments as setup. Optional.
suiteSetup(done)
This function is run once before any test in the suite. Because suiteSetup might need to be asynchronous, you have to call the only argument, done(), when it is finished. Optional.
suiteTeardown(done)
This function is run once after all tests in the suite have finished. suiteTeardown functions are called regardless of whether or not the tests in the suite succeed or fail. It gets the same arguments as suiteSetup. Optional.

An example:

var wrap = require('async_testing').wrap;

var suiteSetupCount = 0;

var suite = wrap(
  { suiteSetup: function(done) {
      suiteSetupCount++;
      done();
    }
  , setup: function(test, done) {
      test.extra1 = 1;
      test.extra2 = 2;
      done();
    }
  , suite:
    { 'wrapped test 1': function(test) {
        test.equal(1, suiteSetupCount);
        test.equal(1, test.extra1);
        test.equal(2, test.extra2);
        test.finish();
      }
    , 'wrapped test 2': function(test) {
        test.equal(1, suiteSetupCount);
        test.equal(1, test.extra1);
        test.equal(2, test.extra2);
        test.finish();
      }
    }
  , teardown: function(test, done) {
      // not that you need to delete these variables here, they'll get cleaned up
      // automatically, we're just doing it here as an example of running code
      // after some tests
      delete test.extra1;
      delete test.extra2;
      done();
    }
  , suiteTeardown: function(done) {
      delete suiteSetupCount;
      done();
    }
  })
})

You can use a combination of sub-suites and wrapping to provide setup/teardown functions for certain tests.