Test First (#1)

Unit testing not only ensures that the code you write is correct, but also helps you develop your code. This can be achieved by testing unimplemented methods and functionalities first, and then “filling in” the code to satisfy the tests. This is the “test first” rule that your XP coworker will always remind you, at any occasion. (You do have an XP coworker, don’t you? :)

The Requirement

Suppose you have a webapp which displays some data from a database. Some of those texts are long, and you are asked to be able to truncate them at predefined lengths on some particular views. So instead of printing “Lorem ipsum dolor sit” you should be able to fit that in 12 chars and print “Lorem ips…”.

Wait

The first worst thing you could do there would be to stick that logic straight into the view (JSP, velocity or whatever templating engine you use). You will not be able to test that piece of logic, nor easily reuse it in some other project. You will need to do this in a Java class and then find a way to call it from the template.

The second worst thing you could do would be to implement this functionality yourself, as it already exists in commons lang. You should know what APIs exist out there and try to reuse as often as possible. But anyhow, we’ll assume that you want to do it yourself.

Think

You start by thinking of where to place this method and whether it will be a helper (static?) method or part of a full fledged class with state etc. Then you choose a good method name and what parameters it will accept. Think of how you would like to use this method.

Code

You write the method signature:

public static String truncate(String text, int length) {
}

Then fill it’s body to the absolute minimum to make it “compilable”:

public static String truncate(String text, int length) {
  throw new RuntimeException("not implemented");
}

…or…

public static String truncate(String text, int length) {
  return "";
}

And then stop, because that’s all you need to implement for now.

Test

You create a test (hit CTRL+SHIFT+U if you use http://www.netbeans.org/) and create a test for the method truncate

public void testTruncate() {
}

This test passes, because it doesn’t test anything. You make things more interesting by adding some basic assertions of what you expect from this method.

public void testTruncate() {
  assertEquals("Lorem ip...", Demo.truncate("Lorem ipsum dolor sit", 11));
  assertEquals("Lorem ips...", Demo.truncate("Lorem ipsum dolor sit", 12));
  assertEquals("Lorem ipsu...", Demo.truncate("Lorem ipsum dolor sit", 13));
}

You run the test and it fails.
You go back to the source code and implement some logic to satisfy this test.

Code

public static final String truncate(String text, int length) {
  return text.substring(0, length-3) + "...";
}

You run the test and it now passes.

Test

It’s time to test for some corner cases.

public void testTruncate() {
  assertEquals("", Demo.truncate("Lorem", 0));
  assertEquals(".", Demo.truncate("Lorem", 1));
  assertEquals("..", Demo.truncate("Lorem", 2));
  assertEquals("...", Demo.truncate("Lorem", 3));
  assertEquals("Lorem ip...", Demo.truncate("Lorem ipsum dolor sit", 11));
  assertEquals("Lorem ips...", Demo.truncate("Lorem ipsum dolor sit", 12));
  assertEquals("Lorem ipsu...", Demo.truncate("Lorem ipsum dolor sit", 13));
  assertEquals("Lorem ipsum dolor sit", Demo.truncate("Lorem ipsum dolor sit", 21));
}

All new assertions fail with a StringIndexOutOfBoundsException. You write code to satisfy these corner cases.

Code

public static final String truncate(String text, int length) {
  switch(length) {
    case 0: return "";
    case 1: return ".";
    case 2: return "..";
    case 3: return "...";
    default:
      return length<text.length() ?
        text.substring(0, length-3) + "..." :
        text;
  }
}

You run the test and it now passes.

Test

Then you test even more.

public void testTruncate() {
  assertEquals("", Demo.truncate("Lorem", 0));
  assertEquals(".", Demo.truncate("Lorem", 1));
  assertEquals("..", Demo.truncate("Lorem", 2));
  assertEquals("...", Demo.truncate("Lorem", 3));
  assertEquals("Lorem ip...", Demo.truncate("Lorem ipsum dolor sit", 11));
  assertEquals("Lorem ips...", Demo.truncate("Lorem ipsum dolor sit", 12));
  assertEquals("Lorem ipsu...", Demo.truncate("Lorem ipsum dolor sit", 13));
  assertEquals("Lorem ipsum dolor sit", Demo.truncate("Lorem ipsum dolor sit", 21));
  try {
    Demo.truncate("Lorem ipsum dolor sit", -1);
    fail("Should have thrown illegal argument exception");
  } catch (IllegalArgumentException expected) { }
}

This is a standard idiom for testing that an exception should be thrown. If this method is called with a negative length parameter, we’d like an IllegalArgumentException to be thrown. It is not mandatory to test for things like these, but some people like to seal their methods from really bad usage.

Code

public static final String truncate(String text, int length) {
  if (length<0) {
    throw new IllegalArgumentException("Length cannot be negative");
  }
  switch(length) {
    case 0: return "";
    case 1: return ".";
    case 2: return "..";
    case 3: return "...";
    default:
      return length<text.length() ?
        text.substring(0, length-3) + "..." :
        text;
  }
}

You run the test and it now passes. You are done.

Conclusion

This implementation might not be the best in the world, but right now this doesn’t matter. The code runs, and it’s robust. Whenever you feel like, you can refactor it and make it perform better. The test will be there to guide you.

p.s What we’ve omitted when we wrote the signature of this method, was to write Javadoc. It is very important to document your API, and we’ll discuss that in another post.

Comments are closed.