On the "Agile Testing" mailing list, Michael Bolton asked for an example of XP-style testing and programming. This example was the result. It represents about 90 minutes of work, counting two phone calls and doing the original writeup.

This problem is taken from a larger project that is building a small database system based on Extended Set Theory, about which we’ll say very little. This particular system is focused on collections of fixed-length records of fixed-length fields, just because that’s what I wanted to work on.

Our problem today is to build an object called RecordMap. This is a class whose instances contain information about the fields of a fixed-length fixed-field record collection. It will need to know about field name, field offset in the record, field width. And the RecordMap object can use the information it has to compute certain control structures that will be used to move around the bytes of the records when the time comes to process records.

The first thing I need to do is to create a RecordMap and see that it has the right stuff in it. I’ve decided that the input description for a record will be a string like this:

   "a:2 b:4 c:2 d:6"

which means that field a is two bytes long, field b is four, c is two, d is six. I’ll go write a test now. Be right back.

 class RecordMapTest < TestCase

   def testCreate
     map = RecordMap.new("a:4")
     element = map.elements[0]
     assert_equal(["a", 0, 4], element)
   end
 end

OK, I just made this up. I decided to start with a RecordMap that maps just one field. And I made up the idea that the RecordMap could return its elements. And I decided that the elements would be three-element arrays, first being name, second offset, third length. As you’ll see, I already know I hate this decision, but it was the easiest thing I could think of.

So I run the test and I get a message that tells me that RecordMap is not defined. So I define the class:

class RecordMap
end

and run the test again.

The error is that the method “initialize” expects zero args and got one. (“initialize” is the standard method for initializing an instance.) So I have to init the object:

class RecordMap
  def initialize(aString)
  end
end

Running the test, now the error is that the “elements” method doesn’t exist.

I have two choices now. One is to just cause that method to answer the fixed result so that the test runs. The other is to actually try to implement the functional code. I’ll go the first way just because it’s more elemental.

class RecordMap
  def initialize(aString)
  end

  def elements
    return [["a", 0, 4]]
  end
end

This technique is called “fake it till you make it”, and basically consists of stubbing the method to return the right thing.

As I look at this, I realize that I’m going to want the result to be stored in an instance variable, which will be computed once and used many times. So I refactor the code to create the instance variable in initialize and return it in elements. I run the tests to be sure it works:

class RecordMap
  def initialize(aString)
    @scopes = [["a", 0, 4]]
  end

  def elements
    return @scopes
  end
end

(The collection is named “scopes” because “scope” is a domain-specific name that Extended Set Theory people would understand. In another environment I might call it fields. Note that @ leads the names of ATtributes in this language (which is Ruby, in case you were wondering.))

OK, now my program works and the code is good. I’ll write another test but I’ll make it easy. I’ll just change the field name.

  def testCreateB
    map = RecordMap.new("b:4")
    element = map.elements[0]
    assert_equal(["b", 0, 4], element)
  end

This test doesn’t run, of course. It says it expects b, 0, 4 but got a, 0, 4. We’re not surprised.

I’m going in smaller steps than I might in real life, but something tells me this is going to turn out interesting, so I’ll go with it. Here’s why: The difference here is just in the first element of the array, so I’ll fix just that element. (Normally I’d probably try to do the whole thing but not this time.) To get the name out, I’ve got to split the string up. Here goes:

  def initialize(aString)
    @scopes = [element(aString)]
  end

  def element(aString)
    name,length = aString.split(':')
    return [name, 0, 4]
  end

See what I did? I left the initialize creating the outer array, and wrote a new method “element”, that splits the string on the colon into name and length (both strings), then stuffs the name into the first element of the result array. That makes both my tests work. I bet you see where I’m going with this, but in case you’re not, here’s my next test:

  def testCreateC
    map = RecordMap.new("c:6")
    element = map.elements[0]
    assert_equal(["c", 0, 6], element)
  end

This time I have a new name /and/ a new length. This will probably get the right name but the wrong length. I’ll run the test to be sure … sure enough, it says

  Expected <["c", 0, 6]> but was:<["c", 0, 4]>

Now to fix that, I just need to get the length, which already got split out:

  def element(aString)
    name,length = aString.split(':')
    return [name, 0, length.to_i]
  end

Note that I remembered to convert the string length to integer with the .to_i. Had I forgotten, the test would have failed.

Now then. I seem to have done everything I can with a single-field definition, so I’ll write a two-field test and see how that works:

  def testCreateTwoFields
    map = RecordMap.new("a:4 c:6")
    a_element = map.elements[0]
    c_element = map.elements[1]
    assert_equal(["a", 0, 4], a_element)
    assert_equal(["c", 4, 6], c_element)
  end

Note that the middle value in the c_element is 4, not zero. This is because the c field starts in byte 4 if the a field starts in byte 0. Remember that the second item in the array is offset in the record. This test doesn’t run. The failure is that there is no element[1] at all, since we just create one element. Now I have to do some work.

After a moment’s reflection, I decide to just split the input string on space, and loop over the resulting substrings (“a:4” and “c:6” in this case), creating an element from each and putting them into the array. I decide to do that in two steps, first changing the init to push the value into scopes instead of just storing it:

  def initialize(aString)
    @scopes = []
    @scopes << element(aString)
  end

All I did here was init the scopes array as empty, and change the next line to push the element into the array. Note that I removed the outer [] in that line, because now I’m adding an element to the array, rather than creating it wholesale. A fanatic might have removed the TwoFields test above to make this work, but I just observed upon rerunning the tests that it was the only one that failed. So I’m still where I was.

Now to do the loop part:

  def initialize(aString)
    @scopes = []
    fields = aString.split(" ")
    fields.each { | fieldString |
      @scopes << element(fieldString)
    }
  end

This changes my error. Now I’m getting a second element. The message says:

  expected:<["c", 4, 6]> but was:<["c", 0, 6]>

If I had been paying attention, I would have foreseen this: of course we’re not cumulating the lengths yet, so all the fields start at zero (since the element method just puts a zero there). But in fact I wasn’t even thinking about that, because I trusted that the test would remind me, as it did.

OK, we need a running total of the lengths. Where shall we put it? I decide that it would be OK to let it be an instance variable of the class. I’ll call it @recordLength.

  def initialize(aString)
    @recordLength = 0
    @scopes = []
    fields = aString.split(" ")
    fields.each { | fieldString |
      @scopes << element(fieldString)
    }
  end

  def element(aString)
    name,length = aString.split(':')
    result = [name, @recordLength, length.to_i]
    @recordLength += result[2]
    return result
  end

This was really tricky: I initialized @recordLength in the initialize method, then used it and incremented it in the element method. My tests are now all running. At this point I’m confident that the code works perfectly for well-formed input strings. That’s where I’ll stop, except that I want to clean up the code a bit. Here’s the whole program before cleanup:

class RecordMap
  def initialize(aString)
    @recordLength = 0
    @scopes = []
    fields = aString.split(" ")
    fields.each { | fieldString |
      @scopes << element(fieldString)
    }
  end

  def element(aString)
    name,length = aString.split(':')
    result = [name, @recordLength, length.to_i]
    @recordLength += result[2]
    return result
  end

  def elements
    return @scopes
  end
end

class RecordMapTest < TestCase

  def testCreate
    map = RecordMap.new("a:4")
    element = map.elements[0]
    assert_equal(["a", 0, 4], element)
  end

  def testCreateB
    map = RecordMap.new("b:4")
    element = map.elements[0]
    assert_equal(["b", 0, 4], element)
  end

  def testCreateC
    map = RecordMap.new("c:6")
    element = map.elements[0]
    assert_equal(["c", 0, 6], element)
  end

  def testCreateTwoFields
    map = RecordMap.new("a:4 c:6")
    a_element = map.elements[0]
    c_element = map.elements[1]
    assert_equal(["a", 0, 4], a_element)
    assert_equal(["c", 4, 6], c_element)
  end
end

I don’t like the fact that element goes into the array to get the length to increment with, and I’m not entirely happy with the .to_i being out of sight there in the array creation. So I refactor that method a bit:

  def element(aString)
    name,lengthString = aString.split(':')
    length = lengthString.to_i
    result = [name, @recordLength, length]
    @recordLength += length
    return result
  end

That suits my aesthetic sense a bit better. Also I don’t like that the initialize method seems to have two levels of code in it. I want to extract that loop. So I do this:

  def initialize(aString)
    @recordLength = 0
    @scopes = []
    initScopes(aString)
  end

  def initScopes(aString)
    fields = aString.split(" ")
    fields.each { | fieldString |
      @scopes << element(fieldString)
    }
  end

I like that a little better. As I do each change, naturally I run the tests. They work, assuring me that the code hasn’t been broken. Here’s the final code and tests. I hope the example was helpful. Any questions will be welcome. I’ll put a question or two of my own below:

class RecordMap
  def initialize(aString)
    @recordLength = 0
    @scopes = []
    initScopes(aString)
  end

  def initScopes(aString)
    fields = aString.split(" ")
    fields.each { | fieldString |
      @scopes << element(fieldString)
    }
  end

  def element(aString)
    name,lengthString = aString.split(':')
    length = lengthString.to_i
    result = [name, @recordLength, length]
    @recordLength += length
    return result
  end

  def elements
    return @scopes
  end
end

class RecordMapTest < TestCase

  def testCreate
    map = RecordMap.new("a:4")
    element = map.elements[0]
    assert_equal(["a", 0, 4], element)
  end

  def testCreateB
    map = RecordMap.new("b:4")
    element = map.elements[0]
    assert_equal(["b", 0, 4], element)
  end

  def testCreateC
    map = RecordMap.new("c:6")
    element = map.elements[0]
    assert_equal(["c", 0, 6], element)
  end

  def testCreateTwoFields
    map = RecordMap.new("a:4 c:6")
    a_element = map.elements[0]
    c_element = map.elements[1]
    assert_equal(["a", 0, 4], a_element)
    assert_equal(["c", 4, 6], c_element)
  end
end

Questions:

  1. Why didn’t I deal with errors in the string format?

Mostly for the scale of the example, but partly because my style of development is to write code that works when it is called correctly. We could talk about what that means and when it’s a good idea, but it’s out of scope for this discussion.

  1. Why didn’t I even test some other examples of good input, like with more fields or something?

Because I’m sure it will work. Want to propose another test that’s within the spec?

Again … further questions are welcome. I hope this is helpful.