Earlier this week I had a piece of JavaScript that I wanted to like this:
var index = someArray.indexOf(someObject);
The problem there is that the indexOf method of
the Array object was introduced in JavaScript 1.6,
which isn't implemented in all browsers
(actually, IE seems to be the real problem here).
Anyway, thanks to the awesomeness of dynamic typing and
prototypeal inheritance
in JavaScript, we can fix that ourselves with code similar to the below.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(item) {
// implementation goes here
};
}
Actually, if you're just looking for the final solution you can
find it easily with your search engine of choice or skip to the
bottom of this article.
But, instead of going through the normal trial and error approach,
I chose to flex my TDD muscle and try to create this method using
a test-first routine. For this exercise, I chose
QUnit as the unit testing framework.
I started with a standard empty testing HTML file where I'll put my test cases and
a reference to my-library.js where I'll add this new method.
<html>
<head>
<title>Tests for mylibrary.js</title>
<link href="testsuite.css" rel="stylesheet" type="text/css" />
<script src="jquery.js" type="text/javascript"></script>
<script src="testrunner.js" type="text/javascript"></script>
<script src="my-library.js" type="text/javascript"></script>
</head>
<body>
<h1>Tests for mylibrary.js</h1>
<h2 id="banner">
<span style="color:#fff;">Result: Red/Green?</span>
</h2>
<h2 id="userAgent"></h2>
<ol id="tests"></ol>
<!--
The #main element is like your test fixture. It's like the
data for your tests.
Whatever exists inside the #main element gets restored
before each test.
Feel free to manipulate the contents of #main in your tests.
-->
<div id="main">
</div>
</body>
</html>
You can put your test cases in a separate .js file and reference it here but
I'll just add the necessary JavaScript to the HTML file itself in this exercise.
Here are the test cases I'm going to support:
- Missing items return -1
- Existing item is found
- Existing item is found with positive starting index
- Existing item is not found with positive starting index
- Existing item is found with negative starting index
- Existing item is not found with negative starting index
- Comparison is non-coercive
Let's start with the first test: Missing items return -1. It's a simple value comparison.
At this point I do not have anything written yet for the indexOf function,
it doesn't even exist if you're on IE. Add the following to the HTML file.
$(function() {
var list = [11, 22, 33, 44];
module('Array.indexOf');
test('Missing items return -1', function() {
equals(list.indexOf(1234), -1);
});
});
When you open the HTML file on IE (I'm using IE8), you'll see this:
Totally expected, but let's try to make this test pass. In the my-library.js file,
let's add the simplest code that can satisfy that test.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function() {
return -1;
};
}
The outer if is there just to prevent replacing the function if
it already exists. The function is clearly incomplete, but that's not the point.
The point is that it passes the test:
Great. Time for the second test: Existing item is found. For that one
we will need to loop through the items in the array. The array is this
in the method. Here's the test, which we add right after the first one.
test('Existing item is found', function() {
equals(list.indexOf(22), 1);
});
And of course that fails
Time to make it pass.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(item) {
for (var i=0; i<this.length; i++) {
if (this[i] == item) {
return i;
}
}
return -1;
};
}
And it passes:
Now let's tackle the third test case: Existing item is found with positive starting index.
This one starts to make things interesting. Here's the test.
test('Existing item is found with positive starting index', function() {
//let's try a few boundary conditions
equals(list.indexOf(33, 0), 2);
equals(list.indexOf(33, 1), 2);
equals(list.indexOf(33, 2), 2);
});
Hmmm. The test passes, as we can see below. Well, that was kind of an accident but
I'll leave the test as is because it does test what was specified. Leaving the
test will help us if we make changes that break this specification.
The next test case is Existing item is not found with positive starting index. We could
write it like this:
test('Existing item is not found with positive starting index', function() {
equals(list.indexOf(33, 3), -1);
equals(list.indexOf(33, 1000), -1);
});
And boom! It fails. It should, we don't even have support for that second parameter yet.
Failing it is, pass it we must. First attempt:
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(item, startIndex) {
for (var i=startIndex; i<this.length; i++) {
if (this[i] == item) {
return i;
}
}
return -1;
};
}
It passes the new test, but it fails another one:
That's actually pretty cool. We would have created a mess if we had just
added the second parameter without some regression testing. Hooray for
unit tests!
What's happening is that my startIndex defaults to undefined
if not passed by the caller. It should default to zero. Easy fix.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(item, startIndex) {
startIndex = startIndex || 0;
for (var i=startIndex; i<this.length; i++) {
if (this[i] == item) {
return i;
}
}
return -1;
};
}
I see the green light!
Our next test case is up: Existing item is found with negative starting index.
What we are trying to do here is allow the caller to specify the starting position as
an offset from the last item using a negative number. So in a 4-element array, passing
-1 means that we start at the last item and passing -3 we start at the second item. See
the test.
test('Existing item is found with negative starting index', function() {
equals(list.indexOf(33, -2), 2);
equals(list.indexOf(33, -3), 2);
equals(list.indexOf(33, -1000), 2);
});
Once again it passes by accident.
The next text case will show that it was an accident:
Existing item is not found with negative starting index. The test for that is below:
test('Existing item is not found with negative starting index', function() {
equals(list.indexOf(33, -1), -1);
equals(list.indexOf(11, -3), -1);
});
It fails:
Making it pass is not that hard, we just need to compute a positive
version of that negative start index.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(item, startIndex) {
startIndex = startIndex || 0;
if (startIndex < 0) {
startIndex += this.length;
}
for (var i=startIndex; i<this.length; i++) {
if (this[i] == item) {
return i;
}
}
return -1;
};
}
Ding, ding, ding! Another passing test for our team.
Lastly, we need to make sure that the Comparison is non-coercive. By
that we mean, a number will never equal a string — or generally speaking, the
things being compared must be of the same type. Let's get some tests that
try to point out tha flaw in our current code.
test('Comparison is non-coercive', function() {
equals(list.indexOf('22'), -1);
});
This fails because, strictly speaking, 22 and '22'
are two different things and we didn't want to match that element like we did.
On to the fix. Can you spot the difference? Try harder.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(item, startIndex) {
startIndex = startIndex || 0;
if (startIndex < 0) {
startIndex += this.length;
}
for (var i=startIndex; i<this.length; i++) {
if (this[i] === item) {
return i;
}
}
return -1;
};
}
And here is the QUnit report in all its successful glory.
If you've never been exposed to TDD this may have felt awkward but
hopefully you noticed that it's more of an evolutionary design
approach, as opposed to trying to make it perfect on the first attempt or
even reckless patching.
It also left us with a valuable collection of tests that we
can execute after making changes to the code, ensuring we didn't
break anything by accident.
Posted
11-12-2009 7:16 PM
by
sergiopereira