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...
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.jsnode-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();
}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.
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])assert.equal(true, value, message);.test.equal(actual, expected, [message])== ).test.notEqual(actual, expected, [message])!= ).test.deepEqual(actual, expected, [message])test.notDeepEqual(actual, expected, [message])test.strictEqual(actual, expected, [message])=== )test.notStrictEqual(actual, expected, [message])!== )test.throws(block, [error], [message])test.doesNotThrow(block, [error], [message])test.ifError(value, [message])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.
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();
}
}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:
suitesetup(test, done)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)teardown
functions are called regardless of whether or not the test succeeds or
fails. It gets the same arguments as setup. Optional.
suiteSetup(done)suiteSetup might need to be asynchronous, you have to call the
only argument, done(), when it is finished. Optional.
suiteTeardown(done)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.