Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

Sponsored Links

Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs

Best practices for test-driven development

Simplify your application of TDD

  • Print
  • Feedback

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.

  • Print
  • Feedback

Resources