We’ve seen how Test-First Development (TFD) moves unit testing to the forefront of the code-writing process.
Test-Driven Development (TDD) is an enhanced version of TFD designed for use with incremental development. The emphasis is on quick cycling between testing and implementation, with each cycle typically a matter of a few minutes.
Test-Driven Development (TDD) is a stronger form of Test-First Development.
In TDD, we repeatedly:
Write just enough of an automated test case for a new desired behavior for the test to fail.
Write just enough new code to pass the test.
Refactor the code to make it acceptable quality.
This ties in very nicely with some of our previous discussion of incremental development. In particular, compare to the way we break stories into tasks.
From Robert Martin
Over the years I have come to describe Test Driven Development in terms of three simple rules. They are:
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
You must begin by writing a unit test for the functionality that you intend to write. But by rule 2, you can’t write very much of that unit test. As soon as the unit test code fails to compile, or fails an assertion, you must stop and write production code. But by rule 3 you can only write the production code that makes the test compile or pass, and no more.
If you think about this you will realize that you simply cannot write very much code at all without compiling and executing something. Indeed, this is really the point. In everything we do, whether writing tests, writing production code, or refactoring, we keep the system executing at all times. The time between running tests is on the order of seconds, or minutes. Even 10 minutes is too long.
We can’t use the same tasks for TDD that we used for TFD. Look again at the tasks we had for TFD:
Perhaps you can see the problem. TDD does not treat steps 1, 2, & 3 above as separate tasks. Instead, it combines them into a single iterative process in which we repeatedly do a little bit of #2, then a little bit of #3, discovering the API (#1) along the way.
So, for TDD, my task breakdown is considerably shorter:
Story: As a spreadsheet designer, I would like to write expressions involving square roots
Write just enough of a test that we fail.
Almost certainly, the point will be when we write this as part of a test:
SquareRootOp sqop;
because that class does not exist.
Write just enough code that we pass.
Add a declaration for a class SquareRootOp
, with no member functions, to the production code.
Write just enough of a test that we fail.
NumericLiteral nine = new NumericLiteral("9");
SquareRootOp sqop = new SquareRootOp(nine);
fails because we do not have a constructor for SquareRootOp
.
Write just enough code that we pass.
Add a constructor:
public SquareRootOp (Expression operand) {
// TODO
}
Write just enough of a test that we fail.
Spreadsheet ss = new Spreadsheet();
NumericLiteral nine = new NumericLiteral("9");
SquareRootOp sqop = new SquareRootOp(nine);
assertEquals (3.0, sqop.evaluate(ss).toDouble(), 0.001);
This compiles but fails when the test is run because we have no body yet for the constructor or for evaluate
.
Write just enough code that we pass.
Implement constructor (to save the Expression passed as the operand) and evaluate
to evaluate the operand and compute the square root,