Sunday, January 29, 2006

Functional Programming in Javascript

Needle (the next version of HttpUnitTest) has been in research mode for quite some time now. I've been mainly trying different paths and see how they feel. One thing that I thought would be great would be able to test javascript as well as HTML. Right now, HttpUnitTest is 100% Smalltalk, but I thought it would be better to able to express tests in Javascript and run the tests in the browser. This is similiar to the approach of Selenium, but I wanted to do it like we did on HttpUnitTest. I love the API of HttpUnitTest. It's simple and allows for more flexible checks. It's also more object-oriented in that I'm not checking text, I'm looking at tagged values.

Here's an example from the HttpUnitTests:
 | browser |
browser := self browserClass on: 'http://localhost:8008/seaside/go/counter'.
self assert: (browser h1 allSatisfy: [:each | each text = '0']).
self assert: (browser a text includesAllOf: #('++' '--')).

browser click: [:a | a text = '++'].
self assert: (browser h1 text includesExactly: #('1')).

browser click: '++'.
self assert: (browser h1 text includesExactly: #('2')).

browser click: '--'.
self assert: (browser h1 text includesExactly: #('1')).

This is a test for the counter example in Seaside. Basically, you create a browser object and point it to an initial url. From there, you navigate by finding a hrefs and form submits. To verify, you simply nagivate the current page in the browser. You do this by simply sending the html element name to each level you want to look at.

For example, when you send the #h1 to the browser object, it returns all h1 elements it finds in a collection. You then, further constrain what comes back by sending the message #text which actually returns a collection of all the contents of the h1 tags. It works exactly like XQuery. But, we're doing it with messages. Pretty cool, huh?

HttpUnitTest makes extensive use of doesNotUnderstand: and generating code on the fly for its simple API. It's a trick that allows us to be more succint and still have all the power of Smalltalk. But, we don't have doesNotUnderstand: in Javascript. What are we to do? I played with several approaches and finally decided, "What if I took a functional approach?" What if I return a function instead of a collection? So, the above example would look like:
find(document, 'h1').includesExactly(['1'])

The initial find function simply is the seed and return functions that we then send in element names. The cool thing is that functions are first class objects in Javascript so they can have other functions and data hanging off of them as well. I was shocked when I first implemented this to only check for element names (other bells and whistles forthcoming) was very short. Here's the code:
    function forEach(array, func) {
for (var i=0; i < array.length; i++)
func(array[i])
}

function findChildrenFromElement(anElement, name) {
var upper=name.toUpperCase()
var left=[anElement]
var result=[]
while (left.length > 0) {
var next=left.shift()
if (next.nodeName == upper) {
result.push(next)
} else {
forEach(next.childNodes, function(each) {
left.push(each)
})
}
}
return result
}
function findChildrenFromArray(anArray, name) {
var result=[]
forEach(anArray, function(each) {
var subResult=findChildrenFromElement(each, name)
forEach(subResult, function(eachResult) {
result.push(eachResult)
})
})
return result
}

function findAll(anArray, aString) {
var innerResults=findChildrenFromArray(anArray, aString)
var result=function(newSearch) {
return findAll(innerResults, newSearch)
}
result.contents=innerResults
return result
}

function find(anElement, aString) {
return findAll([anElement], aString)
}

The seed function find really is simply a call to findAll which is a recursive call to itself, that returns a function calling itself on the next call and the results are stored in an instance variable of the function called contents. The hard worker is findChildrenFromArray. He searches the DOM model for matches of the element name and returns the top results (it stops searching when it finds a match). The rest are support functions.

To expand on this further, the only thing that needs to change is the element name check to make it more general since we're going want to find elements based on attribute values and inner text matches. And wouldn't it be nice to use regular expressions as well (like find me all tags that match h[1-6]). The only thing that would need to change is to send a dicriminator function to findChildrenFromElement instead of the name and use it to check for a match.

I was shocked how little code this was and I have not lost any of the expresssibility of the Smalltalk version. The extra parenthesises are a drag, but it's a small price to pay to test not only your html, but also your javascript. The next research item was to integrate with JSUnit. So, I could use the same framework for testing my web pages. I could test my javascript and html together. I'll keep you posted.

Oh you might be wandering, where is Smalltalk going to be in Needle? Well, right now, my thoughts are it will be driver and it will generate the Javascript needed to test. My thought right now is that I don't want to leave my Seaside environment, but still test everything. I want the feel of HttpUnitTest, but the piece of mind knowing I'm testing not only my html and navigation, but my Javascript as well. I also want Needle in more environments as well. I'm thinking it would be nice to have a version for Java and Ruby. We'll see.

2 comments:

John P said...

Can you make the code font size bigger? It's hard to read the code with the text grey and the font so small. Looks pretty good from what I can read, tho.

Blaine Buxton said...

Done. =)