Examining the Scriptaculous Unit Testing Implementation
7: Setting Up the Tests
Let's move on -- finally :-) -- to the next block of code inside the definition of initialize
:
if(this.options.tests) { this.tests = []; for(var i = 0; i < this.options.tests.length; i++) { if(/^test/.test(this.options.tests[i])) { this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"])); } } } else { // a bunch of stuff we'll get to later. ... }
M'kay. This seems more straightforward than the code we've recently slogged through. If there is a value in the options object for the key "tests", then:
- Create an array tests and make it a member of the current
Test.Unit.Runner
object. - Iterate through the members of
this.options.tests
(we're assuming it's an array, notice) and apply the regular expression/^test/
to each to make sure it is a string that starts with "test". This is a standard convention in xUnit test suites: test functions' names begin with "test". - If this regex test passes, push onto
this.tests
a newTest.Unit.Testcase
, which is created by passing:- the element of
this.options.tests
we are currently iterating with (this should be a string that names a test function) - the value of the element of the hash
testcases
with a key matching the string this.options.tests[i] - the value of the element of the hash
testcases
with the key "setup" - the value of the element of the hash
testcases
with the key "teardown"
- the element of
What is testcases
, again? It's the array of functions that were passed to the constructor for Test.Unit.Runner
and were then passed on to the initialize
function.
So, if there are values in this.options.tests
, it appears that initialize
will use them to determine which of the tests passed to the constructor are used to instantiate Test.Unit.Testcase
objects.
Oh yeah, we have a new pseudoclass, Test.Unit.Testcase
. Let's take a look for the matching constructor. Ah ha. Here we go again. As with Test.Unit.Runner
, the constructor is supplied by calling Class.create()
:
Test.Unit.Testcase = Class.create(); Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), { initialize: function(name, test, setup, teardown) { Test.Unit.Assertions.prototype.initialize.bind(this)(); this.name = name; if(typeof test == 'string') { test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,'); test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)'); this.test = function() { eval('with(this){'+test+'}'); } } else { this.test = test || function() {}; } this.setup = setup || function() {}; this.teardown = teardown || function() {}; this.isWaiting = false; this.timeToWait = 1000; }
As we know from our examination of the instantiation of Test.Unit.Runner
, Class.create()
passes the arguments supplied on to the prototype's initialize
function, which, thanks to Object.extend
, is indeed supplied to Test.Unit.Testcase.prototype
. Notice the wrapped calls to Object.extend
: the methods and members of Test.Unit.Assertions.prototype
are first mixed in to Test.Unit.Testcase.prototype
. And then the methods defined below that are also added to Test.Unit.Testcase.prototype
. That might be important later: Test.Unit.Testcase
can do whatever Test.Unit.Assertions
can do.
In the initialize
function, the four arguments we outlined as a - d earlier are indeed assigned to parameters: name
, test
, setup
, teardown
. The name
, which stands for the name of the test (handy that!) is assigned to the name
value of the this, the Test.Unit.Testcase
object getting instantiated.
Then, there's a bit of processing with test
. If test
is not a function but just a string (maybe also the name of a test?), then some regexes are applied to munge the string according to whether the string contains the word "should" and certain other nearby characters. I'll spare both of us a parsing of these regexes. Let's just make a note that this could be important to revisit later. Finally, this.test
is assigned to a function which, thanks to the great and terrible Javascript eval function, is constructed on the fly using the munged string test
. Again, let's just note that much and back away slowly.
If test
weren't a string (meaning it's likely a function), then the following assignment sets this.test
to either that function or to a function that does nothing in the event that test
is null or otherwise false.
Finally, setup
and teardown
get assigned to member variables of the same name (or to a do-nothing function if these variables evaluate to false). So that's how Test.Unit.Testcase
comes into being.
Test.Unit.Runner
. What happens if this.options.tests
is not defined, i.e. in the like default scenario? The code in the else block happens, that's what:
} else { this.tests = []; for(var testcase in testcases) { if(/^test/.test(testcase)) { this.tests.push( new Test.Unit.Testcase( this.options.context ? ' -> ' + this.options.titles[testcase] : testcase, testcases[testcase], testcases["setup"], testcases["teardown"] )); } } }
Pretty much the same process occurs: this.tests
is built using the testcases
argument. Again, the code checks to make sure the functions are named properly. There is one difference here, though. Notice that the first argument in the call to Test.Unit.Testcase
's constructor, the value that will be given to the name
parameter, is determined by a ternary operator: If there's something called this.options.context
, then the name will be constructed to include what looks like an arrow operator and the value of this.options.titles[testcase]. So there must be a way to "name" the functions with more human-readable names. OK, we've made a note of that too.
There are only three more lines in the initialize
function, but they are action-packed. Well, actually the last line is, and the first two are housekeeping. Let's tackle them quickly.
this.currentTest = 0; this.logger = new Test.Unit.Logger(this.options.testLog);
So now our Test.Unit.Runner
object (that's what this
refers to, remember) has a currentTest
member, initialized to 0. This suggests that the Test.Unit.Runner
object is going to keep track of which test is running, by using an index integer.
In the next line, a logger is instantiated. To be specific, it's an instance of the pseudoclass Test.Unit.Runner
. Care to guess what the constructor for this pseudoclass looks like? You guessed it!
Test.Unit.Logger = Class.create(); Test.Unit.Logger.prototype = { initialize: function(log) { this.log = $(log); if (this.log) { this._createLogTable(); } }, ... }
So once again, the constructor dishes its arguments to the initialize
function defined in the prototype object. This initialize
function doesn't do too much. It assigns this.log
to point to an HTML element with an id that matches the parameter log
. Then, if that assignment worked, another prototpye function, _createLogTable, is called. Glancing at the rest of the Test.Unit.Logger
code, we see a lot of HTML generation, which gives me the confidence to gloss all that code as "stuff that puts log messages in HTML and puts that HTML in the HTML element identified by this.log
.
Finally, here's the last line of initialize
:
setTimeout(this.runTests.bind(this), 1000);
Ah ha! So once initialize
is complete, it actually begins the execution of the tests. So an instance of Test.Unit.Runner
is created, initialized, and executed all at once, from the perspective of the the program that creates it. That is why the code in string_test.html contains nothing but one single function call: new Test.Unit.Runner({ ... })
. That call takes care of everything.
Now, about the line that begins the execution of the tests. It's a little fancy, isn't it? We know what setTimeout
does, right? It essentially sets a timer and then executes some code. In this case, the code is this.runTests.bind(this)
-- what is bind(this)
?
The function bind
is a method of Function
(technically, of the prototype of the constructor function Function
). All functions can have "their" bind
method called. The object passed as an argument becomes this
in the scope of the function when the function executes. This is nice especially in callbacks and other Ajaxy circumstances, in which this
might by default refer to the global window
object. Thanks to the use of bind
we know that when the Test.Unit.Runner
's runTests
method is called, this
will refer to the (still anonymous!) Test.Unit.Runner
object we've been working with all this time.