At the last meeting with Copenhagen.rb, I said I’d test out Crosscheck – a JavaScript testing framework, suited for continuous integration or command-line testing.
I had just tried getting it to run, following the slightly outdated Add Event tutorial. At last I got it to work, but I had to actually read the text, not just paste the examples. Better grab the example source code than read the slightly-outdated docs.
Tonight I have tested it in a little more real setting: I had a hunch of how I would be able to do a filtering function that worked like in Quicksilver or Textmate. A case-insensitive, very inclusive search that just narrows down based on the sequence of the characters, not on their immediate order. (Did that make sense?)
Expressed as a positive test case: “jy8” should match “Jyri is happy to be 8 years of age”.
Anyway, I thought I could construct a regex on the fly, somehow. I itched to try. And I wanted to try it using tests along the way.
So, I entered the first, simplest case I could think of: equality. A list of one element should return only a single-element array. That was simple to do, so I could take on the next (that I could think of). And the rest was just implementation, until I hit something I had to test in Firebug.
The cool thing was that my experimentation was over as soon as I’d answered the question I had. Focus, the programmer’s holy grail. The promise of focus, at least. We’ll see about how that holds up in practice.
Here is the quite redundant, stupid list of test cases that I accumulated while trying things on:
crosscheck.onSetup(function(){
crosscheck.load("scripts/quicksilverSearch.js");
});
crosscheck.addTest({
testShouldFindSameMatchingString : function () {
var outPut = quicksilverMatch('johnny', ['johnny']);
var exp = ['johnny'];
assertEquals( exp[0], outPut[0],
'The same word could not be found in the simple array. Shame.' );
},
testShouldNotFindANonMatchingString : function () {
var outPut = quicksilverMatch('jx', ['johnny']);
assertTrue( outPut.length == 0, 'The word jx was found in johnny?! Shame.' );
},
testShouldMatchAllOnFirstLetter : function() {
var outPut = quicksilverMatch('jo', ['johnny', 'jools', 'june']);
assertTrue( outPut.length == 2,
'Unable to match with indexOf on the first two letters. Shame.' );
},
testShouldMatchAcrossWords : function() {
var outPut = quicksilverMatch('jy', ['jyri', 'johnny', 'june']);
assertTrue( outPut.length == 2,
'Unable to match across words. Shame.' );
},
testShouldMatchAcrossWordsCaseInsensitively : function() {
var outPut = quicksilverMatch('jy8', ['Jyri 8 years', 'Johnny turns 8', 'june']);
assertTrue( outPut.length == 2,
'Unable to match case-ins. across words. Shame.' );
}
});
So, the first thing that gets done before execution of each test case: load my script. That is specified in crosscheck.onSetup
.
The least interesting part of this is my resulting code. After pulling it through JSLint and mulling over it a couple more times, it just shrunk. Which is the end goal of testing, I guess: to have me own as little code as possible. (We’ll come back to that.)
/**
* "Quicksilver" filter search: returns an array of the haystack elements that
* match case-insensitively the given term.
* 2007-10-05, Olle
*/
function quicksilverMatch(term, haystack) {
var matches = [];
var straw; // The individual piece of hay
// "t.*e.*r.*m" regular expression for loose matching
var re = new RegExp(term.split("").join('.*'), "i");
for (var idx = 0, len = haystack.length; idx < len; ++idx) {
straw = haystack[idx];
// Direct match inside word? Or: a case-ins. loose match
if (straw.indexOf(term) > -1 || re.test(straw)) {
matches.push(straw);
}
}
return matches;
}
The take-home piece I got from this sitting was how to run Crosscheck. I created a wrapper shell script that I christened “crosschecka”, where I set my target browsers (“hosts”) and paths:
#!/bin/bash
#
# Run the crosscheck jar on given test path, or default
#
# We run on Firefox 1.5 and IE6: moz-1.8:ie-6
#
# Author: olle, 2007-09-26
CROSSCHECK_JAR=/Users/olle/lib/crosscheck-0.2.1/crosscheck.jar
# Colon-separated. Possible values are moz-1.7:moz-1.8:ie-6
HOSTS_TO_CHECK=moz-1.8:ie-6
# Our test path is tests/eosweb/js, relative to the web app root
if [[ $1 == '' ]]; then
{
java -jar $CROSSCHECK_JAR -hosts=$HOSTS_TO_CHECK tests/eosweb/js
exit 0;
}
else
{
java -jar $CROSSCHECK_JAR -hosts=$HOSTS_TO_CHECK $1
exit 0;
}
fi
(I never got the file test in the else clause right, so I let Crosscheck complain for me.) The Rails-type win here is having a pre-set default, so I never have to think about it.
Thought: Maybe this whole testing thing is a way to get me to reduce the scope of my methods? To be testable, they have to be decomposed to parts that are cheap to test.
I said I’d come back to owning less code: now that I know these things about the behaviour of my code, do you think I should revisit and sharpen up my test-cases? For one thing, some of them might not even hit right. The first one, it seems very weak and situationally dependent.
Would you go back and change such stuff? Or delete such cases?