Let's Make a Test Framework
Welcome to part 39 of Let’s Make a Framework, the ongoing series about building a JavaScript framework.
If you haven’t been following along, these articles are tagged with lmaf. The project we’re creating is called Turing.
Last week we continued building turing-test.js, a basic unit testing framework. The idea behind this framework is to build something that follows the CommonJS specifications and works in modern browsers. This will help us test Turing more effectively.
The source code for last week’s tutorial can be found in commit 9a4c33e79cbeb0bf2a71. This week’s code is in commit 8dd48b5b6da1e26ab7f1.
Testing Exceptions
CommonJS Unit Test 1.0 defines assert.throws pretty briefly:
// Expected to throw an error:
assert.throws(block, Error_opt, message_opt);
The error and message arguments are optional. The error argument should check the type of the exception, and the message is an optional message.
When I was reading through Node’s assert.js I noticed they also define doesNotThrow. I debated the usefulness of this for a while, but I realised there are a lot of cases where testing for no exception is more meaningful than a simple assert.ok, and it should make the tests for the throw assertion easier to write.
If we’re going to implement that as well, we’ll need a generic throws() function with an option to determine if an exception was expected.
The basic throws function, called by the assertions, looks like this:
function throws(expected, block, error, message) {
// Set up
try {
block();
} catch (e) {
actual = true;
exception = e;
}
// Test outcome
If an error was expected and one has been passed, we can test the exception matches like this:
exception.constructor != error
In this case, error is expected to be an exception constructor like CustomException:
function CustomException() {
this.message = 'Custom excpetion';
this.name = 'CustomException';
}
assert.throws(function() {
throw new CustomException();
}, CustomException);
Exception Assertion Pattern
The structure of the throws and doesNotThrow method looks like this:
- Setup
- Check if the error parameter is actually a message
- Run the function to test and capture the results in a
try/catch - If an exception was thrown and one wasn’t expected, fail
- If an exception was not thrown and one was expected, fail
- If an exception was thrown but doesn’t match the specific type, fail
Putting this together with the above snippets, we get something like the following:
function throws(expected, block, error, message) {
var exception,
actual,
actual = false,
operator = expected ? 'throws' : 'doesNotThrow';
callee = expected ? assert.throws : assert.doesNotThrow;
if (typeof error === 'string' && !message) {
message = error;
error = null;
}
message = message || '';
try {
block();
} catch (e) {
actual = true;
exception = e;
}
if (expected && !actual) {
fail((exception || Error), (error || Error), 'Exception was not thrown\n' + message, operator, callee);
} else if (!expected && actual) {
fail((exception || Error), null, 'Unexpected exception was thrown\n' + message, operator, callee);
} else if (expected && actual && error && exception.constructor != error) {
fail((exception || Error), null, 'Unexpected exception was thrown\n' + message, operator, callee);
}
};
assert.throws = function(block, error, message) {
throws.apply(this, [true].concat(Array.prototype.slice.call(arguments)));
};
assert.doesNotThrow = function(block, error, message) {
throws.apply(this, [false].concat(Array.prototype.slice.call(arguments)));
};
Please excuse the crudity of this model, I didn’t have time to build it to scale or to paint it.
Notice that we can use the expected result to determine the callee. That means the fail method will correctly track the operator and start stack function.
Tests
I promised tests of tests, so here they are. This is actually an interesting example because it illustrates how useful doesNotThrow is — we can use it to test the inverse without doing any hacking to simulate failed exceptions.
exports['test throws'] = function() {
assert.throws(function() {
throw 'This is an exception';
});
function CustomException() {
this.message = 'Custom excpetion';
this.name = 'CustomException';
}
assert.throws(function() {
throw new CustomException();
}, CustomException);
assert.throws(function() {
throw new CustomException();
}, CustomException, 'This is an error');
};
exports['test doesNotThrow'] = function() {
assert.doesNotThrow(function() {
return true;
}, 'this is a message');
assert.throws(function() {
throw 'This is an exception';
}, 'this is a message');
};
The two and three argument versions are being tested here as well.
Object.keys
I noticed the code I ported from Node in last week’s tutorial included Object.keys. Some browsers don’t have that, so I wrote a little function to provide the functionality.
function objKeys(o) {
var result = [];
for (var name in o) {
if (o.hasOwnProperty(name))
result.push(name);
}
return result;
}
Conclusion
That’s all of the assertions! We’ve discovered a few interesting things here, most notably that defining an inverse for assert.throws makes testing the assertions easier.
If you’re reading this in the future and you want to check out this version of the project, it’s commit 8dd48b5b6da1e26ab7f1.