Best practices for test-driven development

Simplify your application of TDD

Test-driven development has received much attention in the last few years. Many books have been written on the subject, including books by Kent Beck (Test Driven Development), Johannes Link (Unit Testing in Java), and David Astels (Test Driven Development: A Practical Guide). We found test-driven development (TDD) to be an extremely valuable development strategy. However, TDD takes some getting used to before it can be employed effectively. In this article, we share some best practices we discovered that helped facilitate our application of TDD. We'll begin with a background on mock objects, an essential component for our application of TDD, and a topic we refer to throughout the article.

Mock object background/definition

Mock objects have been well covered in various literature, notably in Astels' Test Driven Development: A Practical Guide. We won't repeat that discussion here; instead we provide a brief description of mock objects for readers unfamiliar with them.

First, let's define a production class as a class required for the correct operation of the product under development. Typically, any class shipped with the product is considered a production class. Unit tests are written to test production classes. Production classes often interact with other objects (let's call them collaborators) that are passed in as constructor or method arguments. When unit testing a production class, you should focus on the production class's responsibilities. You can take for granted that the collaborators work as expected. After all, those collaborators have their own unit tests—you don't need to repeat them. You really just want to test that the interaction with those collaborators occurs as expected.

That's where mock objects come in—a mock object takes the collaborator's place during execution of a unit test. In a mock object, you hard-code the behavior you need for a specific unit test of your production class. For example, a production collaborator might interact with an external system (e.g., a database) to return a list of records, while a mock object hard-codes this list in the class definition. Other mock objects might generate boundary-value conditions that would be difficult to simulate otherwise (e.g., a mock object that represents an integer value might return a value of Integer.MAX_VALUE). A mock object contains only the logic required to make a unit test pass. Thus, mock objects tend to be significantly simpler than the real production classes used in your system.

Our mock objects typically return a hard-coded value from method invocations and record information about which methods were called and what parameters were passed to them. We test our production classes in terms of mock objects by passing mock objects to the production class as constructor and method arguments. Typically, testing the production class simply involves verifying that it makes the right method calls on the mock objects and that it correctly interprets the results of those calls.

An illustrative example:

public class CalculationComponent {
   public CalculationComponent (CalculationStrategy calcStrategy) {
      this.calcStrategy = calcStrategy;
   }
   public int doCalculation(int num) {
      return calcStrategy.calculate(num);
   }
}
public interface CalculationStrategy { 
   int calculate(int num); 
}
public class MockCalculationStrategy implements CalculationStrategy {
   public boolean calculateWasCalled = false;
   public int num;
   public int calculate(int num) {
      this.calculateWasCalled = true;
      this.num = num;
      return 87;
   }
}
public class CalculationComponentTest extends TestCase {
   public void test1() {
      MockCalculationStrategy mcs = new MockCalculationStrategy();
      CalculationComponent cc = new CalculationComponent(mcs);
      int result = cc.doCalculation(5);
      assertTrue(mcs.calculateWasCalled);
      assertEquals(5, mcs.num);
      assertEquals(87, result);
   }
}

Although this example looks (and is) contrived, a large percentage of the unit tests we write are as simple as the one above. Creating such mock objects probably looks mundane, and it is. Some toolkits simplify mock object creation, notably EasyMock. For more information on mock objects, read Astels' Test Driven Development: A Practical Guide.

Now let's discuss best practices for applying test-driven development.

Best Practice 1: Create simple constructors

As alluded to above, mock objects are an essential part of writing unit tests. We usually end up with far more mock objects than production classes. Unless you plan on extracting an interface whenever you need a mock object (this creates many needless interfaces, complicating understanding of your design), many of your mock objects will extend concrete production classes. The creation of these mock objects will be simplified if the production class has constructors that only set fields or construct simple types.

You've probably been exposed to classes that do a lot of work in their constructors. On a recent project, we were exposed to constructors that performed file system I/O, created threads, and threw exceptions. Classes that perform such work in their constructors complicate unit-test writing. Unit tests need to run quickly and without dependency on external systems. Mock objects are used to ensure fast execution and to avoid external dependencies. However, to create a mock object for a concrete class, you must extend the class and call super() to delegate to the parent class's constructor. If that parent class's constructor performs complex work that cannot be overridden, you're stuck. Additionally, if the parent class's constructor throws exceptions, your mock object subclass must also throw those exceptions, since you cannot call super() in a try/catch block. This ends up complicating your test code, because your test code must now catch or throw this exception when constructing the mock object.

Our solution is to create constructors that only set fields based on their input parameters and/or perform simple construction (e.g., instantiating a data type like java.lang.String). When more complicated actions are required to set up a class (including actions that might involve exceptions being thrown), create a factory class to perform these actions.

Even if you always create simple constructors, most likely you'll remain exposed to classes that lack simple constructors and need mock objects. For example, let's say you need to create a mock object for the following class, A (which most likely was not developed following the TDD approach):

class A {
   public A() throws IOException {
      this.thread = new Thread();
      this.fileWriter = new FileWriter(new File("out.txt"));
   }
}

Note how this class can easily be restructured into the following two classes:

public class A {
   public A(Thread thread, FileWriter fileWriter) {
      this.thread = thread;
      this.fileWriter = fileWriter;
   }
}
public class AFactory {
   public A getA() throws IOException {
      Thread thread = new Thread();
      FileWriter fileWriter = new FileWriter(new File("out.txt"));
      return new A(thread, fileWriter);
   }
}

Now it is trivial to create a mock object for class A, because class A's constructor does nothing substantive with its input parameters. Your mock object can simply pass in null for these parameters and override A's other methods to perform the behavior it wants. Not only is it simpler to mock up object A, it's also simpler to test A. The unit tests for A can now focus primarily on A's responsibilities (i.e., its methods), leaving the tests related to constructing A in AFactory's unit tests.

Note
Although you are more likely to see a class like the original class A when dealing with legacy code, in our experience, it is possible to create such a class even while following a TDD approach. The problem with class A may not become apparent until you need to create a mock object for A. However, once you are familiar with this issue, you are unlikely to create complicated constructors in future use of TDD.

Remember that we created this factory to simplify mock object creation for unit tests. Using factories to abstract away the complexities of creation into a separate class is a common best practice of object-oriented programming. This leads us to an interesting observation: A common discovery when doing TDD is that when you produce code easier to test, you've also produced higher-quality code. TDD steers you towards using proper object-oriented methodologies.

One final note on this topic: We do sometimes create concrete production classes that throw RuntimeExceptions if required constructor parameters are not provided (e.g., if a constructor argument is null, an IllegalArgumentException might be thrown). Assuming no parent interface for this concrete class exists (and assuming it doesn't make sense to extract such an interface), your mock object is stuck overriding a concrete class whose constructor throws exceptions (albeit RuntimeExceptions, which aren't part of the method's signature). In the next section, we discuss how to create mock objects for such a class.

Best Practice 2: Create helper mock objects that throw RuntimeExceptions

Since mock objects are so essential to test-driven development, you'd like their development to be as simple as possible. This can be tricky when you need to create many mock objects for a large interface or class, as can often happen when adding unit tests around legacy code (legacy, in this sense, means code not developed with testing in mind—in our experience, interfaces/classes developed with testing in mind tend to be smaller and more focused).

An easy solution to this problem is to create a helper mock-object subclass that implements/extends the interface/class and provides an implementation for every method that simply throws a RuntimeException, e.g., throw new RuntimeException("Operation not supported"). A good IDE can easily create such a class for you. Use this class as the base class for all future mock objects of this type. The mock objects that derive from this base mock class will only need to override the methods of the parent class relevant for the particular test in which they are used. An additional benefit is that if you accidentally forget to override a method necessary for the test, your test will fail with a thrown RuntimeException. This immediately identifies which additional method your mock object needs to override.

Let's return to our example of a constructor that throws RuntimeExceptions if required constructor arguments are invalid. In such a case, we create a helper mock-object subclass that provides valid constructor arguments to the superclass. For example:

public class Group {
   private final String groupName;
   public Group(String groupName) {
      if (groupName == null) 
         throw new IllegalArgumentException("groupName cannot be null");
      }
      this.groupName = groupName;
   }
   // Group methods...
}
public class MockGroup extends Group {
   public MockGroup() 
      super("");
   }
}

Now to create mock subclasses for Group, you can extend MockGroup and not worry about exception conditions in Group's constructor.

Best Practice 3: Use immutable objects

An immutable object is an object that cannot be modified after creation. When at all possible, you should define classes to be immutable. This simplifies your class and its testing. When classes are mutable, the number of test cases tends to be much higher than in immutable classes. Mutable objects can have different internal states throughout their lifetime, all of which need to be tested explicitly. For example, when testing a mutable class, you often need to test the case where a get() accessor method is called before the associated set() method has been called. These test cases do not exist for immutable classes. Writing immutable classes for your clients also simplifies client code development—the client code's author need not be concerned with messy aliasing problems. This simplifies not only development, but also testing of the client code, as we'll see below.

Note
There are different levels of immutability and varying opinions on what immutability really means. We will consider classes like java.lang.String to be immutable, even if String's implementation contains mutable fields (e.g., to cache an index into the string, for example). String's important data cannot be modified after creation, so from a client's perspective, String is immutable.

For our purposes, the ideal form of a class is one with only final fields to immutable objects. This guarantees that an instance of the class can never change. The Group class above is an example of such a class.

Even if you cannot create a class in this ideal form, you should still strive to create classes with all final fields—at least you'll know each field's reference cannot change after construction, eliminating numerous test cases. When accessor methods are necessary, return a copy of the member data (unless the data is already immutable) so a client cannot modify your class's private data. Here's an example of one such mutable class we developed:

public class ActivationDefinitionBuilder {
   // A List of ActivationTask objects
   private final List activationTasks;
   public ActivationDefinitionBuilder() {
      this.activationTasks = new ArrayList();
   }
   public ActivationDefinition getActivationDefintion() {
      // ActivationTasks are immutable, so ArrayList's shallow clone is sufficient
      return new ConcreteActivationDefinition(this.activationTasks.clone());
   }
   public void addActivationTask(ActivationTask activationTask) {
      this.activationTasks.add(activationTask);
   }
}

This class has a single final field, which is mutable (the List can be modified). The getActivationDefinition() method passes a copy of the list to the ConcreteActivationDefinition constructor to prevent that class from modifying ActivationDefinitionBuilder's private data. The getActivationDefinition() method does not need to perform a deep copy of the list, since the ActivationTask type (not shown) is immutable. This reinforces the point mentioned earlier—the client code (in this case, ActivationDefinitionBuilder) is simpler and easier to test because ActivationTask is immutable.

In our experience, nearly all classes can be written in the "ideal" fashion. On a recent project, we created roughly 200 new production classes, and 95 percent had only final fields to immutable objects. The classes that did not meet this criteria were either builder classes, such as the one seen above, or classes representing a physical connection to a device (with open() and close() methods). Of the 200 new classes, 99 percent had only final fields. Little conscious effort was required for us to create so many classes of this form. Once we discovered how immutability eases the testing burden, creating immutable or nearly immutable classes whenever possible became natural.

Best Practice 4: Use pair programming

We've found pair programming and TDD to be mutually complementary. Before diving into pair programming, we should clarify our application of TDD—for us, TDD means test-first development. We always write the test prior to writing the production code required to make the test pass. Writing tests first drives our code's development and design. Our attempts at test-last development were not successful for reasons we'll discuss shortly.

In our experience, doing TDD for the first time is hard. We frequently got into situations where we were unsure what to do next. Often the person at the keyboard would revert back to old behaviors and implement functionality prior to a test requiring it. Even after having practiced TDD for months, we still experience the temptation to write code prior to writing the unit test. Having a back-seat driver, as it were, to help keep us focused on TDD is a huge benefit to us. For example, the back-seat driver might ask, "What test should we write next?" "What are this object's responsibilities?" or "What are we trying to test?" When applying TDD, you must carefully consider these questions, which are easy to lose track of when applying TDD for the first or even tenth time. We're convinced that our success in employing TDD is in large part due to our pairing.

We also attempted to do pairing without using test-first development. We tried test-last development while pairing. For us, that meant implementing a fair chunk of functionality prior to writing any unit tests, then retroactively creating the necessary tests. We sometimes went a long time without writing unit tests. We found many limitations to this approach. The person sitting in the backseat frequently became bored, and the person driving did all of the development and thinking about the next piece of functionality to implement. The two developers rarely verbally communicated. We found it too easy to lose track of our objective, plus there was a much greater chance we'd need a debugger to track down problems in our code.

Test-first development, in contrast, gives us clear next objectives: identify the next test we want to write, create the mock objects we need, and implement the necessary production code to make the test pass. The back-seat driver typically thinks about the next test to write and the necessary mock objects, while the person driving focuses on getting the current test to pass. The developers communicate more, as the back-seat driver keeps the front-seat driver focused on the current task, which is well understood between both developers.

We found pairing to be productive when employing test-first development. We were pleased with our code quality and unit-test coverage, and felt our time was well spent. In contrast, when pairing without doing test-first development, we were less pleased with the resulting code and unit tests, and felt much of the person's time in the backseat was poorly spent. We found that if pairing and test-first development are not used together, the perception of pairing as "two coders doing the job of one" may be true—effectively reducing productivity by half. But if the two practices are used together, each person's role and usefulness is fully maximized. For us, TDD and pairing complemented as well as justified each other.

We hope these best practices for employing TDD help you use TDD efficiently in your next development project.

Michael Grove and Brooks Bollich are software engineers for HP. Grove graduated with a BS in computer science from Carnegie Mellon University in 2000, and Bollich has a BS in computer science from the University of New Mexico. Both have worked extensively in software engineering best practices, including the study of design patterns, refactoring, pairing, and test-driven development.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies