Examining the Scriptaculous Unit Testing Implementation
4: The initialize Function
We understand that the constructor provided for new Test.Unit.Runner
calls the new object's initialize method, and it passes to initialize an array of arguments--an array that, as our subsequent analysis shows, happens to have one member, a big ol' hash. So let's look at the definition of initialize
, which is available from Test.Unit.Runner
's prototype:
initialize: function(testcases) {...}
Gosh, it sure is lucky that Test.Unit.Runner
's prototype defines an initialize function
, isn't it? I mean, what if it didn't? The constructor would bomb out, wouldn't it? Why, yes. If I were documenting the prototype.js code, I might include a comment about this, along the lines of "Every pseudoclass whose constructor is returned with Class.create()
must provide an initialize function in its prototype object." But that's just me.
"Hold on a second," you may be insisting, frowning again. "The prototype definition, which includes this initialize
function, appears after the call to new, which means that this.initialize.apply(...)
is happening before initialize exists. How is that possible?"
I had the same reaction, because I am not yet fully thinking in JavaScript. But what I realized is that initialize is indeed defined before new and Class.create()
call it. How? Because Class.create()
returns a function, but that function is not executed until later, and not until, to be specific, the code
new Test.Unit.Runner(...);
in string_test.html executes, by which point all the global-level assignments in unittest.js have been made, including the definition of initialize.
The function that Class.create()
returns, and which serves later as the constructor for the request for a new Test.Unit.Runner
, is a closure.
A closure is, for our purposes, an object that has its own behavior and its own scope. By behavior, I mean the function body. By "its own scope" I mean that when it's defined, and when the value of "this" is set, that value, along with the values of any other variables, are saved with the function object. When the function is called, that scope, like the function's behavior, is still there, ready to go.
All functions in JavaScript are closures. You don't usually notice anything odd about them, if you find this kind of behavior odd (if you have been programming a lot in, say, Java). It's when these functions are returned by other functions or assigned to persistent variables, when they outlive a function call, that things get interesting. Ajax is especially helped out by the use of closures as callbacks--it would be harder to do Ajaxy things in languages without this feature.
Closures are actually a fascinating topic, and I am confident they're going to become more important for developers, but this isn't the time to talk about them in depth. Suffice it to say that the constructor isn't called until everything about Test.Unit.Runner
has been defined.
OK, let's go on with our tracing of the execution of the constructor. It's led us to Test.Unit.Runner
's initialize function, which we now know is defined for us by the time the constructor fires up. Here's the first bit of initialize:
initialize: function(testcases) { this.options = Object.extend({ testLog: 'testlog' }, arguments[1] || {}); this.options.resultsURL = this.parseResultsURLQueryParameter(); this.options.tests = this.parseTestsQueryParameter(); ... }
What is Object.extend(...)
, and what is it doing with the complicated chunks of code getting passed to it as arguments?
I turn to my outstanding JavaScript reference, David Flanagan's O'Reilly book JavaScript: The Definitive Guide, 5th Edition. There I discover... nothing. According to the JavaScript, er sorry, ECMAScript standard, there is no method extend
for Object
. So where is this coming from?
From prototype, it turns out. Right below the assignment of our friend Class.create:
Object.extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; }
"Umm," you ask nervously, "Is Sam Stephenson adding a method to JavaScript's Object
class, the pseudoclass that acts as a superclass for all JavaScript objects?"
He sure is, because he's a hacker version of Chuck Norris, and you know what that means.
Note, though, that he isn't meddling with Object.prototype
, which defines the things that all instances of Object
, that is just about everything, receives. No, extend
is basically a class method, and so it's only called as Object.extend(...)
, not myObj = new Object(); myObj.extend(...);
So it's not as crazy as it could be.
Why did Sam decide to place the function here, as a method of Object
? Well, because he's going to use it all over the place, so he wants it globally accessible. Object
is always accessible. Placing the function as a method of Object
also gives it a namespace--it won't conflict with some other extend function we may happen to define, maybe in the global scope. Also, as we'll see, this function is used to make objects that, in a way, extend other objects, so what semantically better place is there for this function than in Object
?
OK, so that's what Object.extend
is. Now what does it do?
It seems to take two arguments, which it names suggestively as destination and source. Then it iterates over source with a for in loop. If source is a hash or an object, then the for in loop is iterating over the names, or labels, of its members, its methods and variable members. It assigns each label, in turn, to property, and then creates a member in destination with the same name and assigns the value of the source's property to this new destination property. Then it returns the destination object, which now contains source's goodies.
In other words, it adds the source object's stuff to the destination object. This is what is called a mixin: without defining a formal object-oriented relationship, like inheritance, between these two objects, we "mix" one's properties into the other's.
So what's getting mixed into what in testunit.js? Here's the code again:
this.options = Object.extend({ testLog: 'testlog' }, arguments[1] || {});
The "destination" object is a hash with one key/value pair: { testLog: 'testlog' }
. The source is... well, it's determined by what looks like an or expression: arguments[1] || {}
. What's this?
Well, in JavaScript, as in many nice languages that are designed to help you get things done, something like arguments[1] || {}
returns a value. If the value of the left hand side of the ||
operator is not false, then that value is returned. If not, then the value of the right hand is returned (even if the value of the right hand side is false, because then the or expression indeed returns false).
But, what's arguments[1]
? Only one argument, the hash with all the testcases defined in string_test.html, is being passed. In this case, arguments
has only one member, arguments[0]
, so arguments[1]
evaluates to undefined
, which is interpreted in Javascript (in this context) as the value false
. That means that Object.extend()
is being passed an empty hash as its source object and thus iterates over nothing. The destination object, that little hash with one key/value pair, is not altered, and it is returned, as is, to be assigned to this.options
.
But arguments
could have had a second member, which we can guess would have been some kind of hash that might have included, among other things, another value for the key 'testLog'. What can the scriptaculous site tell us about this tantalizing prospect?
It shows us an example of instantiating a Test.Unit.Runner
with a second argument, options
, that would be passed along via this.initialize.apply(this, arguments)
to the code we were just looking at. Here's the example of the use of options:
new Test.Unit.Runner( { ... // test cases follow, each method which starts // with "test" is considered a test case testATest: function() { with(this) { // code }}, testAnotherTest: function() { with(this) { // code }} }, { options } );
And what about this options
hash? What information does the scriptaculous site have to offer?
"The options section is optional. (fixme: describe options)"
Ah ha. A mystery. Well, we'll go on anyway, and maybe we can discover what some of the possible key-value pairs for options
might be. Here are the next two lines in string_test.html, which contain two functions I'm going to call "parse" functions, because they have that word at the beginning of their names:
this.options.resultsURL = this.parseResultsURLQueryParameter(); this.options.tests = this.parseTestsQueryParameter();
So, we're calling other methods of Test.Unit.Runner
. I type "methods" because that's how they look and behave--like member methods. They are, of course, just functions that are contained in the prototype object that's being defined for use as a template for Test.Unit.Runner
. And functions are just data. Just a reminder. It sounds as though these two methods will parse some value, and the first will return a something that can be interpreted as a URL, whereas the second is going to return a collection of objects.
Let's look at the definitions of the two "parse" functions and see what they do:
parseResultsURLQueryParameter: function() { return window.location.search.parseQuery()["resultsURL"]; }, parseTestsQueryParameter: function(){ if (window.location.search.parseQuery()["tests"]){ return window.location.search.parseQuery()["tests"].split(','); }; },
Both of these functions are making use of an interesting string of symbols:
window.location.search.parseQuery()
. What is parseQuery
and what does this do? The short answer is that parseQuery
is an extension to the String
prototype that attempts to read the string as a URL to identify the parameters that follow the question mark--the query string--and then returns a hash of hey/value pairs from the query string.
If you're (still) reading this to get a general grasp on testunit
, you might want to take this gloss on parseQuery
and skip over the next section, in which I take a more detailed look at this line of code and go kind of surprisingly (and tangentially) deep into the workings of prototype.js. In doing so I cover a lot of interesting points about JavaScript and the thinking of the prototype authors, but you may not be longing for that right now.