Guideline: Test Driven Development (TDD)
Relationships
Related Elements
Main Description

Topics

What is TDD?

TDD is the practice of writing unit tests and production code concurrently and at a very fine level of granularity. A pair of programmers first write a small portion of a unit test, and then they write just enough production code to make that unit test compile and execute. Then they write a little bit more of the test and then add enough production code to make that new bit compile and pass. This cycle lasts somewhere between 30 seconds and five minutes. Rarely does it grow to ten minutes. In each cycle, the tests come first. Once a unit test is done, the pair goes on to the next test until they run out of tests for the task they are currently working on.

A TDD Example in Java

What follows is a simple example of test-driven development. The program we are writing is a text formatter that can take arbitrary strings and can horizontally center them in a page. The first column shows the tests, and the second column shows the production code. The test is always written first and compiled. If the compile fails, then production code is added to make the compile succeed. Then the test is run to see if it passes. If the test fails, then production code is added to make the test pass. If the test passes, then a new test is added.

First we write the test
Then we write the production code

public void testCenterLine(){ 
    Formatter f = new Formatter();
}

does not compile

class Formatter{
}



compiles and passes

public void testCenterLine(){ 
    Formatter f = new Formatter(); 
    f.setLineWidth(10);              
    assertEquals("   word   ", f.center("word"));
}




does not compile

class Formatter{ 
    public void setLineWidth(int width) { 
    } 


    public String center(String line) {
return ""; } }

compiles and fails
 

import java.util.Arrays;


public class Formatter { 
    private int width;              
    private char spaces[]; 
    
    public void setLineWidth(int width) { 
        this.width = width; 
        spaces = new char[width]; 
        Arrays.fill(spaces, ' '); 
    } 

   
    public String center(String line) { 
        StringBuffer b = new StringBuffer();
        int padding = width/2 - line.length(); 
        b.append(spaces, 0, padding);              
        b.append(line); 
        b.append(spaces, 0, padding); 
        return b.toString();              
    }
}


compiles and unexpectedly fails
 

public String center(String line) { 
    StringBuffer b = new StringBuffer();              
    
int padding = (width - line.length()) / 2; 
    b.append(spaces, 0, padding);              
    b.append(line); 
    b.append(spaces, 0, padding); 
    return b.toString();              
}



compiles and passes

public void testCenterLine() {
    Formatter f = new Formatter();                
    f.setLineWidth(10); 
    assertEquals("   word   ", f.center("word"));
} 


public void testOddCenterLine() { 
    Formatter f = new Formatter();
    f.setLineWidth(10); 
    assertEquals( "  hello    ", f.center("hello"));
}


compiles and fails

public String center(String line) { 
    
int remainder = 0; 
    StringBuffer b = new StringBuffer(); 
    int padding = (width - line.length()) / 2;
    
remainder = line.length() % 2; 
    b.append(spaces, 0, padding); 
    b.append(line);
    b.append(spaces, 0, padding + 
remainder); 
    return b.toString(); 
}



compiles and passes

What are the benefits of TDD?

  • Test Coverage. If you follow the rules of TDD, then virtually 100% of the lines of code in your production program will be covered by unit tests. This does not cover 100% of the paths through the code, but it does make sure that virtually every line is executed and tested.
  • Test Repeatability. The tests can be run any time you like. This is especially useful after you've made a change to the production code. You can run the tests to make sure you haven't broken anything. Having the tests to back you up can give you the courage to make changes that would otherwise be too risky to make.
  • Documentation. The tests describe your understanding of how the code should behave. They also describe the API. Therefore, the tests are a form of documentation. Unit tests are typically pretty simple, so they are easy to read. Moreover, they are unambiguous and executable. Finally, if the tests are run every time any change is made to the code, they will never get out of date.
  • API Design. When you write tests first, you put yourself in the position of a user of your program's API. This can only help you design that API better. Your first concern, as you write the tests, is to make it easy and convenient to use that API.
  • System Design. A module that is independently testable is a module that is decoupled from the rest of the system. When you write tests first, you automatically decouple the modules you are testing. This has a profoundly positive effect on the overall design quality of the system.
  • Reduced Debugging. When you move in the tiny little steps recommended by TDD, it is hardly ever necessary to use the debugger. Debugging time is reduced enormously.
  • Your code worked a minute ago! If you observe a team of developers who are practicing TDD, you will notice that every pair of developer had their code working a minute ago. It doesn't matter when you make the observation! A minute or so ago, each pair ran their code, and it passed all its tests. Thus, you are never very far away from making the system work.

What are the costs of TDD?

  • Programming in tiny cycles can seem inefficient. Programmers often find it frustrating to work in increments that are so small that they know the outcome of the test. It sometimes seems that such a tiny step is not worth taking.
  • A lot of test code is produced. It is not uncommon for the bulk of test code to exceed the bulk of production code by a large amount. This code has to be maintained at a significant cost.
  • A lot of time is spent keeping the tests in sync with the production code. Programmers sometimes feel that time spent on keeping the tests working and well structured is time that is not being spent on the customer's needs.

What testing principles should I employ?

  • Isolation. When writing a unit test for a module, consider whether you want that module to invoke other modules. If not, then isolate the module with interfaces. For example, suppose you are testing a module that interacts with the database. The test has nothing to do with the database; it simply tests the way that the module manipulates the database. So you isolate the module from the database by creating an interface that represents the database and that the module uses. Then, for the purposes of the test, you implement that interface with a test stub. This kind of isolation greatly decreases the amount of coupling in the overall system.

  • Simplicity. Keep your edit/compile/test cycles extremely short: less than five minutes on average. Write only enough production code to make the current tests pass. Try not to write code that will make future tests pass. At every edit/compile/test cycle, keep the code as simple as it can be.
  • Increase Generality. As you add test cases, the production code should become more and more general. Always try to increase generality. For example, consider the following test case:
     public testThreeSquared() {     assertEquals(9, MyClass.square(3)); }
    

    We might make this test pass by writing:

     public class MyClass {  public static int square(int n) {      return 9;  }               }

This conforms to the simplicity principle. If testThreeSquared were the only test case that mattered, then this implementation would be correct. Of course, we know that it is incorrect, but in its current form it verifies that the test case actually passes when it is supposed to. Now suppose that we add a new test case:

 public testFourSquared() {      assertEquals(16, MyClass.square(4));  }         

We could make this pass by changing the square function as follows:

 public static int square(int n) {      if (n == 3)  return 9;      else  return 16;  }

While this would pass the test, it violates the rule to make the code more general. To make the code more general, we have to return the square of the argument.

 public static int square(int n) {       return n*n;  }

This solution passes all the tests, is simple, and increases the generality of the solution.

  • Corner Cases and Boundary Conditions. Corner cases and boundary conditions are implemented in the production code with if statements or other similar decision structures. Don't write these statements unless you have a unit test that is failing because they don't exist. For example, let's say you are calculating weekly pay for an hourly employee.
     public void testHourlyPay() {      double hourlyRate = 10.00;      double hoursWorked = 8;      Employee e = new Employee(hourlyRate);      assertEquals(80.00, e.calculatePay(hoursWorked));           }
    

    The code that makes this pass looks like this:

 public class Employee {      private double hourlyRate;        public Employee(double hourlyRate) {  this.hourlyRate = hourlyRate;      }        public double calculatePay(double hoursWorked) {  return hourlyRate * hoursWorked;      }  }

Now let's say we want to calculate overtime pay. Any hours over eight are charged at time-and-a-half. The first thing we do is add the new failing test case:

 public void testOvertime() {      double hourlyRate = 10.00;      double hoursWorked = 10;      Employee e = new Employee(hourlyRate);      assertEquals(110.00, e.calculatePay(hoursWorked); }

Then we make the test case pass by changing the production code.

 public double calculatePay(double hoursWorked) {      double overtimeRate = hourlyRate * 1.5;      double normalHours = Math.min(hoursWorked, 8.0);                   double overtimeHours = hoursWorked ̵; normalHours;      return (normalHours * hourlyRate) + (overtimeHours * overtimeRate);  }

Avoid adding any if, while, for, do, or any other type of conditional without a failing test case. Remember to add test cases for each such boundary condition.

  • Test Anything That Could Possibly Break. By the same token, don't bother to test things that cannot possibly break. For example, it is usually fruitless to test simple accessors and mutators.
     public void testAccessorAndMutator() {      X x = new X();      x.setField(3);               assertEquals(3, x.getField());  }
    

    Accessors and mutators cannot reasonably break. So there's no point in testing them. Judgment clearly has to be applied to use this rule. You will be tempted to avoid a necessary unit test by claiming that the code cannot possibly break. You'll know you've fallen into this habit when you start finding bugs in methods you thought couldn't break.

  • Keep Test Data in the Code. It is sometimes tempting to put test data into a file, especially when the input to a module is a file. However, the best place for test data is in the unit test code itself. For example, assume we have a function that counts the number of characters in a file. The signature for this function is:
     public int count(String fileName).
    

    In order to keep the test data in the unit test code, the test should be written this way:

  •  public testCount() {      File testFile = new File("testFile");               FileOutputStream fos = new FileOutputStream(testFile);      PrintStream ps = new PrintStream(fos);      ps.print("Oh, you Idiots!");      ps.close();               assertEquals(15, FileUtil.count("testFile"));      testFile.delete();           }
    

This keeps all the data relevant to the test in one place.

  • Test Pruning. Sometimes you'll write tests that are useful for a time but become redundant as other tests take over their role. Don't be afraid to remove old redundant tests. Keep the test suite as small as possible without compromising coverage.
  • Keep Test Time Short. The effectiveness of the tests depends upon convenience. The more convenient it is to run the tests, the more often they will be run. Thus, it is very important to keep the test run time very short. In a large system, this means partitioning the tests.

    When working on a particular module, you'll want to choose the tests that are relevant to that module and the surrounding modules. Keep the test time well under a minute. Ten seconds is often too long.

    When checking in a module, run a test suite that tests the whole system but takes no more than 10 minutes to run. This may mean you'll have to pull out some of the longer running tests.

    Every night, run all the tests in the system. Keep the running time small enough so that they can be run more than once before morning just in case there is a problem that forces a rerun.

How do I test GUIs?

The trick to writing unit tests for GUIs is separation and decoupling. Separate the GUI code into three layers, typically called Model, View, and Presenter:

  • The Model understands the business rules of the items that are to be displayed on the screen. All relevant, business-related policies are implemented in this module. Therefore, this module is easy to test based solely on its inputs and outputs.
  • The Presenter understands how the data is to be presented and how the user will interact with that data. It knows that there are buttons, check boxes, text fields, etc. It knows that sometimes the buttons need to be disabled (grayed), and it knows sometimes text fields are not editable. It knows, at a mechanical level, how the data are displayed and how the interactions take place. However, it does not know anything about the actual GUI API. For example, if you are writing a Java Swing GUI, the Presenter does not use any of the swing classes. Rather, it sends messages to the View to take care of the actual display and interaction. Thus, the Presenter can be tested, again, based solely on its inputs from the Model and its outputs to the View.
  • The View understands the GUI API. It makes no policy, selection, or validation decisions. It has virtually zero intelligence. It is simply a shim that ties the interface used by the Presenter to the GUI API. It can be tested by writing tests that check the wiring. The tests walk through the GUI data structures, making sure that the appropriate button, text fields, and check boxes have been created. The tests send events to the GUI widgets and make sure the appropriate callbacks are invoked.

How do I test embedded systems?

Some software is written to control hardware. You can test this software by writing a hardware simulator. The tests set the hardware simulator up into various states and then drive the system to manipulate that hardware. Finally, the tests query the simulation to ensure that the hardware was driven to the correct final state.

How do I test concurrency?

Some software is reentrant or concurrent. Race conditions can make the software behavior non-deterministic. There are failure modes that can be both severe and strongly dependent upon timing and order of events. Software that works 99.999% of the time can fail that last .001% of the time due to concurrency problems. Finding these problems is a challenge.

Usually exhaustive Monte Carlo testing is used to attempt to drive the system through as many states as possible.

Once concurrency problems are discovered, tests can be written that drive the system to the failure state and then prove the failure. Thereafter, the problem can be repaired, and the test remains in the test suite as a regression test.

How do I test database transactions?

Almost always the best way to do this is to create an interface that represents the database. Each test case can implement that interface and pretend to be the database, supplying its own data and interpreting the calls made by the module under test. This prevents test data from actually being written and read from the database. It also allows the test code to force failure conditions that are otherwise hard to simulate.

See: http://c2.com/cgi/wiki?MockObject

How do I test Servlets?

Servlets are simply pipes through which form data passes into a program and HTML passes out. The trick to testing a servlet is to separate the program from the pipe. Keep the servlet code as thin as possible. Put your program in plain old classes that don't derive from Servlet. Then you can test those plain old classes as usual. If the servlet itself is thin enough, it may be too simple to bother testing.

Of course, you can also set up your own little servlet invoker or use one of the open source versions. These programs act like a web server and fire servlets for you. You pass the form data to them, and they pass the HTML back to you.

See:

http://c2.com/cgi/wiki?JunitServlet
http://c2.com/cgi/wiki?ServletTesting
http://strutstestcase.sourceforge.net/

How do I test web pages?

An HTML document is almost an XML document. There is a tool that allows you to query an HTML document as though it were an XML document. That tool is called HTTPUnit. Using this tool, you can write tests that inspect the innards of an HTML document without worrying about white space or formatting issues. Another tool called HTMLUnit also does something similar. HTMLUnit includes support for testing HTML pages with embedded JavaScript.

See:

http://httpunit.sourceforge.net/
http://htmlunit.sourceforge.net/