February 26, 2004 - Test-Driven Development and Agitator-Driven Refactoring

We've had a good solid month of development experience on The eXperiment now and things are going rather well. All of our code is written test first by pairs, we refactor without mercy, and we continuously integrate. Part of our continuous integration is continuous agitation, and it is on that front that we've had the biggest surprise.

Our code is tested, and therefore testable, and Agitator eats it up. We've set 100% coverage as our goal and most of our classes reach 90-100% with little effort. We've also set the goal of 100% assertion coverage, and while this requires a bit more effort, we've currently got 94% up on our whiteboard. The numbers are nice, but it was more fun last Friday when Agitator unearthed a StackOverflowError in our code, code that already had both unit tests and acceptance tests. (Actually there were two places that could trigger the stack overflow, and Agitator caught them both.) But that's not the surprise, that's the product working as expected.

What surprised me, and what has me most excited right now, is how Agitator is driving our refactoring.

A typical example of this was the case of assigning points to one of our objects. The object was accumulating points from multiple sources and had a method something like:

  public void addPoints(int points) {
    score += points;
  }

Simple enough, right? Agitator generated the expected observation (which I made into an assertion) that score is changed by the value of points. Things were a little more interesting when I looked at the class-level observations and saw that score was frequently negative — which, by definition, shouldn't be the case. I edited the observation to create the assertion score >= 0, which of course failed. Now that I had a failing test I could change the code to check points:

  public void addPoints(int points) {
    if (points < 0) {
      throw new IllegalArgumentException("points shouldn't be negative");
    }
    score += points;
  }

My assertion passes, yay, but now there's a new outcome under addPoints — IllegalArgumentException — that lacks an assertion. My style is to verify that I'm getting the assertion I expect by testing the message, and following the "once and only once" principle I introduce a constant. This gives the assertion @EXCEPTION.getMessage().equals(POINTS_CANT_BE_NEGATIVE) to match the code:

  public void addPoints(int points) {
    if (points ‹ 0) {
      throw new IllegalArgumentException(POINTS_CANT_BE_NEGATIVE);
    }
    score += points;
  }

A simple case, but it turned out to be well worth the effort. We had a system test fail with the exception "points shouldn't be negative", and when we investigated, it turned out there was a subtle but systemic bug in our algorithm and we ended up reimplementing part of it (which we did fearlessly given our excellent unit test coverage).

If you're practiced and conscientious with JUnit this process probably sounds very familiar. I know I've written the moral equivalent test before:

  public void testAddNegativePoints() {
    try {
      myclass.addPoints(-2);
      fail();
    } catch(IllegalArgumentException expected) {
      assertEquals(POINTS_CANT_BE_NEGATIVE, expected.getMessage());
    }
  }

So why the excitement?

I didn't need to think of the test!


Posted by Jeffrey Fredrick at February 26, 2004 03:52 PM


Trackback Pings

TrackBack URL for this entry:
http://www.developertesting.com/mt/mt-tb.cgi/113


Comments

Post a comment




Remember Me?