Examining the Scriptaculous Unit Testing Implementation

9: The Tests Themselves

In previous pages, I've explored the underdocumented unit testing code that comes with Scriptaculous. But now it's time to show how the unit testing code is applied and used. In short, it's time for the tests themselves.

How do you set up the tests? Here's what I did.

  1. Start an HTML document. Include prototype.js and unittest.js
  2. Set up a place for test log to display, e.g. a DIV with id "testlog"
  3. Add test code right into document:
    <script type="text/javascript" language="javascript" charset="utf-8">
    // <![CDATA[
      new Test.Unit.Runner({
        failureTest: function() { with(this) {
          assertEqual(2, 4); // failures are more interesting
        }}
      });
    // ]]>
    </script>
  4. Load the page, and: 0 tests, 0 assertions, 0 failures, 0 errors Status Test Message Uh oh. What happened?

Well, I violated a rule I should have known about: I did not begin my test name with "test". Let me try again:

testFailure: function() { with(this) { ...
1 tests, 0 assertions, 1 failures, 0 errors

Cool. But why is the naming convention enforced, when I am explicitly defining functions to be run as tests? Is it possible or useful that some other function, helpers perhaps, might be included?

new Test.Unit.Runner({

  halfOf: function(num) { return num-1; },

  testFailure: function() { with(this) {
    assertEqual(2,halfOf(4));
  }}

});
ReferenceError: halfOf is not defined(ReferenceError: halfOf is not defined)

Nope.

But there are the special functions setup and teardown, which are handled in the TestRunner.initialize function:

initialize: function(testcases) {
  ...
  for(var testcase in testcases) {
    if(/^test/.test(testcase)) {
      this.tests.push(
         new Test.Unit.Testcase(
           ..., 
           testcases[testcase], testcases["setup"], testcases["teardown"]
         ));
    }
  }
  ...
}

Each new Testcase is passed a reference to the setup and teardown functions, if they exist. Let's try something.

setup: function() { with(this) {
  this.halfOf = function(num) { return num-1; };
}},

testFailure: function() { with(this) {
  assertEqual(2,this.halfOf(4));
}}
Failure: assertEqual: expected "2", actual "3"

Success! Well, test failure, but that's what we wanted. My experiment also explains why the test case function blocks are wrapped in "with(this){...}". Because "'this' is adulterous" in Javascript, and we want to fix it to the object being initialized, the TestRunner. (Actually 'this' is just misunderstood. Functions are first-class objects in JS, so within a function used as a closure, 'this' often refers to the global scope's this value: the window).

It was a little reckless of me to add this "halfOf" function right into the TestRunner object: I could have overwritten something previously defined. I check the Runner's info in Firebug, and I see there is no predefined "helpers" member object, so I think I will create it and add my function there.

setup: function() { with(this) {
  this.helpers = {
    'halfOf': function(num) { return num-1; }
  };
}},

testFailure: function() { with(this) {
  assertEqual(2,this.helpers.halfOf(4));
}}

Ah... That feels better.

By the way, my concern about overwriting a member object or function points to an interesting possibility. I could intentionally overwrite one of TestRunner's methods, like assertMatch or status, if I needed something special.

Now I'll write a test that has something to do with the code I want to write. I started looking into unit testing in the first place because I want to develop a browser for displaying multiple "items" in a preview or thumb format. I'm going to write the test first, according to the behavior I want from my as-yet unwritten code.

Let's see. The first thing I need is a good way to create such a browser. Starting with the trivial, I'll specify a constructor. I'm going to say that if the constructor is not told otherwise, it should set the number of items in the browser to 5.

testBrowserSetup: function() {  with(this) {
  var b = new Browser();
  assertEqual(5, b.number_of_items);
}}
ReferenceError: Browser is not defined (ReferenceError: Browser is not defined)

That's helpful. If you're used to programming in Javascript, you know how vague (or non-existent) the error information you get can be.

Let's add some code:

function Browser() {
  this.number_of_items = 5;
}
passed testBrowser Setup 1 assertions, 0 failures, 0 errors

Now, we should be able to send a number of items as an argument, like this:

testBrowserSetup: function() {  with(this) {
  var b = new Browser(3);
  assertEqual(3, b.number_of_items);
}}
Failure: assertEqual: expected "3", actual "5"

Good. We meant to do that.

Test a little. Code a little:

function Browser(number_of_items) {
  this.number_of_items = (number_of_items || 5);
}
passed testBrowserSetup 1 assertions, 0 failures, 0 errors

That will do it for now. From here I know how to go on to write tests as I code.