Test-driving a HashMap
Right, here’s a TDD* walk-through, creating a new class tests-first.
HashMap
I’ve deliberately picked a fairly uninteresting class, as I want to focus on the process. It’s a utility data-holding object found in Java, more formalised than the ubiquitous Object that’s found in Flash applications the world over. That I’m stealing from Java is good, as we don’t need to design it – we just need to implement the same API:
- size():Number
- put(key:Object, value:Object):Object //returns previous value associated with key, or null
- get(key:Object):Object //returns value mapped to key
- clear():Void //empties the HashMap
- containsKey(key:Object):Boolean
- containsValue(value:Object):Boolean
- isEmpty():Boolean
- keySet():Array //returns an Array of keys
- putAll(hashmap:HashMap):Void //puts the contents of another HashMap into this one
- remove(key:Object):Object {//returns previous value associated with key, or null
- values():Array //returns an Array of values
Handily that API gives us our to-do list for the exercise (though we won’t get through it all).
Setting up the project
Firstly, if you don’t have asunit already, have a read-through this post which sets it up.
Set up a directory for this exercise. Create a new Actionscript 2.0 fla** in there called hashmap_tester.fla.
Add two lines to the first frame:
-
import AllTests;
-
var at:AllTests = new AllTests();
Now test the movie.
It complains that it can’t find AllTests, so let’s create that:
-
class AllTests extends com.asunit.framework.TestSuite {
-
private var className:String = "AllTests";
-
-
public function AllTests() {
-
super();
-
//addTest(new HashMapTest());
-
}
-
-
}
and now testing throws no errors, and we’re good to think about the HashMap.
Where to start?
Why are we even going down this route? Well, I don’t know about you, but I have a habit of thinking about a class for 5 minutes and then jumping straight into coding it. On a large/formal project, I plan enough to come up with a convincing overview of the likely class structure, but don’t drill down to a detailed level. Look, a class diagram, let’s get coding!
So, other folks’ motivations may be different, but I follow a test-driven process because
- I need something to prevent me coding before I’ve thought about design, and
- I’m sick of leaving all thought of testing to the end of a project when we’re already squeezed for time
Therefore, we’re going to start with a single method from the Java API: size(), and define how we want it to behave (actually, the Java boys have already done that):
size():int – Returns the number of key-value mappings in this map.
Whilst that’s already clear, it’s not specific enough to write a test (humour me). So, let’s agree that calling size() on an empty HashMap will return 0, calling size() on a HashMap with a single entry will return 1, one with 2 entries will return 2, and so on. I know I’m stating the bleeding obvious, but these are the statements that will prove our job is done.
Setting up the testing class
TDD uses unit tests, which verify single units of functionality, so we end up with a single class (HashMap) being tested by a single test class (HashMapTest).
-
class HashMapTest extends com.asunit.framework.TestCase {
-
private var className:String = "HashMapTest";
-
-
public function testIt():Void {
-
assertTrue("failing test", false);
-
}
-
-
}
Now uncomment the line addTest(new HashMapTest()); in AllTests.as, and test the movie. We get a red bar and the following message:
0 out of 1 asserts passed ---------------- Item Failed at : assertion : assertTrue message : failingtest methodName : testIt className : HashMapTest
That’s the information we need – which class the problem was in, which method, which assertion, etc. Well, alright, we could have worked all that out, but once there are lots of classes, these pointers will be invaluable.
The rules of TDD are really simple: Red, Green, Refactor.
We’ve got red, which when considering we started with nothing is already a major achievement, so we need to take the next step. Change false to true in the assertTrue statement, and run the movie again. Whatcha got?
Green is cool, calming, and the place to be. In TDD, we start with red, get to green as quickly as possible, and stay there.
Since there isn’t much refactoring to be done with this test, let’s move on to writing a test for size(), starting with an empty HashMap.
Testing size()
-
public function testSize():Void {
-
var instance:HashMap = new HashMap();
-
assertEquals("empty HashMap.size() == 0", 0, instance.size());
-
}
Run it, and you get a compile error, which handily points out our next move: let’s create the HashMap class and give it a size() method returning a Number.
-
class HashMap {
-
-
public function HashMap() {}
-
-
public function size():Number {
-
return null;
-
}
-
-
}
Testing shows we’re at red, which is progress.
Now all we need to do is get to green, in the simplest and quickest (and dirtiest) way possible. So, I’m changing return null to return 0.
Obviously, I’m tricking the test, because we should be testing that there are no entries in there, but that comes with refactoring. For now we’re just aiming to pass the test, and we do the simplest thing to achieve that.
What’s next: calling size on a HashMap with a single entry returns 1.
But, before we write that test, we need to think about how to put an entry into the HashMap
Putting something into the HashMap
put(key:Object, value:Object):Object //returns previous value associated with key, or null
So, again starting with the tests: the behaviour we’re specifying is:
- putting an element (with key “element1″ and value “value1″) into an empty HashMap returns null
- putting an element (with key “element1″ and value “value2″) into the HashMap returns the first element (key:”element1″, value:”value1″)
- putting an element (with key “element2″ and value “value2″) into the HashMap returns null
-
public function testPut():Void {
-
var instance:HashMap = new HashMap();
-
var elementKey:String = "element1";
-
var elementValue:String = "value1";
-
-
assertNull("putting element into empty HashMap returns null", instance.put(elementKey, elementValue));
-
}
Since we know the compiler’s going to baulk, let’s avoid that by adding the following to HashMap.as:
-
public function put(key:String, value:Object):Object {
-
return key;
-
}
Note that I’m returning key to force the test to fail. And fail it does, so change it to return null; and we’re green. Time to add another assertion. Just duplicate the first, so the same key is being added to the HashMap again, but change the assert itself to assertNotNull.
Dang, fix one thing, break another. Well, this is where the refactoring part of the triad comes in: we need to refactor the code so that it passes both tests. (”Refactor” is much better than “write”, don’t you agree? It means that the code is already written, but just needs changing. And that’s the point – it is already written in my head).
So what to do? Well the HashMap is essentially a wrapper around a Plain Old Actionscript Object, isn’t it? If we’re going to return the first element when we add the second, we need to keep those elements somewhere, so let’s create a private object to hold them in the HashMap.
-
class HashMap {
-
-
private var map:Object;
-
-
public function HashMap() {
-
map = new Object();
-
}
-
-
public function size():Number {
-
return 0;
-
}
-
-
public function put(key:String, value:Object):Object {
-
return null;
-
}
-
-
}
Right, if a key doesn’t already exist in the HashMap, return null, but if it does return its previous value. How should that look?
-
public function put(key:String, value:Object):Object {
-
var returnObj:Object = null;
-
if(map[key] != undefined) {
-
returnObj = map[key];
-
}
-
map[key] = value;
-
return returnObj;
-
}
Well, give it a go, since I’m coding without thinking too much, and let the tests decide. And they say ‘PASS’ – all is green. Fantastic. Let’s add the final part of our tests for put to HashMapTest:
-
public function testPut():Void {
-
var instance:HashMap = new HashMap();
-
var elementKey:String = "element1";
-
var elementValue:String = "value1";
-
-
assertNull("putting element into empty HashMap returns null", instance.put(elementKey, elementValue));
-
assertNotNull("putting element into empty HashMap returns null", instance.put(elementKey, elementValue));
-
assertNull("putting element into empty HashMap returns null", instance.put("element2", "value2"));
-
}
And it still works! That’s cool.
Size() re-visited
So now we are able to put elements into the HashMap and have them stay there, we can end the trickery that’s getting us past the size tests.
Add another assert to testSize in HashMapTest.as:
-
public function testSize():Void {
-
var instance:HashMap = new HashMap();
-
assertEquals("empty HashMap.size() == 0", 0, instance.size());
-
instance.put("key", "value");
-
assertEquals("HashMap with 1 element, size() == 1", 1, instance.size());
-
}
That’s the test. I’m happy. It compiles and the asunit test runner shows red. I’m happier.
What is size() supposed to give us? Information about how many elements are in the HashMap, i.e. how many elements are in the private variable map. So we can just iterate through that, and keep a count of how many properties it has:
-
public function size():Number {
-
var count:Number = 0;
-
for(var p in map) {
-
count++;
-
}
-
return count;
-
}
And I reckon that should pass.
To-do list
I’ve missed a good test out here – now we know size() is working, then we can use it to verify put’s behaviour. Specifically, if we add elements using different keys, size will increase each time; but if we add elements using the same key, then they will replace each other and the size will remain the same.
It’s handy to have a list – paper- or computer-based – nearby to record such thoughts when they appear, since you’re likely to be in the middle of writing a test or refactoring code to pass a test. It also allows you to prioritise, and to go home knowing you can return in the morning and know where to pick up.
Which reminds me. Always finish the day with a failing test. The next morning you turn up, run the tests, and know exactly what you were going to do next. That’s easy, isn’t it? (N.B. If you’re working in a team with source code control – you are, aren’t you? – do not check in this failing test, that might upset the others).
Homework
Well, when did someone last give you homework to do?
You write out the tests to cover the rest of the Java HashMap API. And, if you really fancy it, code the implementation too.
The TDD rhythym
1. Quickly add a test.
2. Run all tests and see the new one fail.
3. Make a little change.
4. Run all tests and see them all succeed.
5. Refactor to remove duplication.
Red, green, refactor.
Red, green, refactor.
Red, green, refactor.
* Test-Driven Development
** I’m currently working on a project porting AS1.0 to AS2.0, nary a sign of AS3.0. I believe that there are other AS2-ers out there who need information about unit-testing and test-driven development, so I’m fairly unapologetic about using AS2.0. If you are only coding AS3.0 (lucky you!) and can’t find resources, shout and I’ll have a stab at re-writing this for AS3.0, using flexunit.