Examining the Scriptaculous Unit Testing Implementation

8: Let's Run Us Some Tests

Now we come to the execution of the tests themselves. Here is the definition of runTests.

runTests: function() {
  var test = this.tests[this.currentTest];
  if (!test) {
    // finished!
    this.postResults();
    this.logger.summary(this.summary());
    return;
  }
  if(!test.isWaiting) {
    this.logger.start(test.name);
  }
  test.run();
  if(test.isWaiting) {
    this.logger.message("Waiting for " + test.timeToWait + "ms");
    setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
  } else {
    this.logger.finish(test.status(), test.summary());
    this.currentTest++;
    // tail recursive, hopefully the browser will skip the stackframe
    this.runTests();
  }
}

Most of this is fairly straight-forward, so let me do my disco version of a summary, and then I'll point out some interesting features.

Remember how we set this.currentTest to 0 before calling runTests? In the first line of runTests, we use it as an integer array index to pick the first test in this.tests. If the index was beyond the upper bound of this.tests, then the tests have all run, and a couple of things happen related to showing results and a summary before the function returns. Otherwise, the function continues. If no test is waiting, the logger records the beginning of the next test. Then, the next test is run. There's another check to see whether a test is waiting. If there is, a timeout is set, after which, runTests is called recursively. If no test was waiting, then the logger "finishes" logging this test, this.currentTest is incremented, and runTests is called recursively.

What's interesting about this code? First of all, this.runTests is handled in two different ways. As a setTimeout argument, it is this.runTests.bind(this). A few lines lower it is simply called: this.runTests(). In the first instance, the function is being passed as an argument, not called. It is being treated as an object. When Test.Unit.Runner.runTests is executed later, it is executed in the context of the window object. Without the bind trick, this will refer to the global window object, not our Test.Unit.Runner.

Also, notice the little comment above the recursive execution of runTests:

// tail recursive, hopefully the browser will skip the stackframe

Two things to point out in this gem of documentation. One: "tail recursive". Yes, JavaScript was inspired by Scheme. As in Scheme, you get lexical scoping and closures, both of which make recursion a lot more fun. Tail recursion (disco version) is recursion executed with a call to the original function after the current call to the function has completed its business. Yes, I'm sure it's a lot more complicated than that--I'm only wrapping up The Little Schemer right now. I may come back to this post after I finish the Wizard Book and drop a lot more science about recursion. Or I might simply be so transcendently enlightened at that point that blog posts and articles will seem too bound to the plane of striving. We'll see.

Second thing: "Hopefully?" I don't entirely understand this bit about skipping the stackframe, but "stackframe" sounds important, and it sounds as though if this skipping weren't to "hopefully" happen, it could be Bad. One drawback to recursion is that is can demand a lot of memory on the stack, as the calls pile up. Tail recursion is supposed to help. Maybe that's what the "hopefully" is about. I'm going to make a note about this, and I will try to find out more later.