DailyJS

Let's Make a Test Framework

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks testing lmaf

Let's Make a Test Framework

Posted by Alex R. Young on .
Featured

tutorials frameworks testing lmaf

Let's Make a Test Framework

Posted by Alex R. Young on .

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:

  1. Setup
  2. Check if the error parameter is actually a message
  3. Run the function to test and capture the results in a
    try/catch
  4. If an exception was thrown and one wasn't expected, fail
  5. If an exception was not thrown and one was expected, fail
  6. 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
.

References