Topics
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.
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();
}
|
class Formatter{
}
|
public void testCenterLine(){
Formatter f = new Formatter();
f.setLineWidth(10);
assertEquals(" word ", f.center("word"));
}
|
class Formatter{
public void setLineWidth(int width) {
}
public String center(String line) {
return "";
}
}
|
|
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();
}
|
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"));
}
|
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();
}
|
-
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.
-
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.
-
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.
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.
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.
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.
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.
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.
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
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/
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/
|