Test Driven Development Introduction Test Driven development (TDD) is a fairly recent (post 2000) design approach that originated from the Extreme Programming / Agile Methodologies design communities. Its primary goal is to produce better code by introducing the testing process earlier into the overall design process. In TDD, tests become the design. The official book on TDD is Test-Driven Development by Example, by Kent Beck (see references). It outlines the philosophy behind TDD as well as providing an excellent example of TDD in actual use. Test-Driven Development follows the general script test (fail); code the simplest thing to pass the test; test (pass); refactor The goal of TDD is clean code that works, according to Ron Jeffries. Some of the advantages of TDD include: - It is predictable. You know when you are finished without worrying about hidden bugs. - It gives you time to reflect on the code you are writing. First you write tests, then you write code to pass the tests, then you reflect on the code and refactor it to make it better. - It improves the lives of the people using the software - It allows people to count on you, and you on them. - It feels good to write it From Test-Driven Development by Example, by Kent Beck In TDD, there are two simple rules: 1. Write new code only if an automated test fails 2. Eliminate Duplication TDD follows on many of the principles of Extreme Programming (and Agile Programming). A good synopsis of Extreme Programming can be found here http://ootips.org/xp.html Another goal of TDD is for the programmer to become test infected. That is, the programmer becomes convinced of the benefits of writing tests first and so starts thinking of programming in terms of what test would illustrate this requirement instead of how should I code this requirement. Hopefully, after working through the example I ve provided, and doing TME4, you too will see value in TDD. Fibonacci Example Using TDD, let us build a class to provide the Fibonacci series for any given input, n. The Fibonacci series is {1, 1, 2, 3, 5, 8, 13, 21, 34, 55,. The value of n starts at n=0 and increments, such than the Fibonacci number for n=3 (for example) is 3. Fibonacci (4) = 5, Fibonacci (8) = 34 and so on. To start our development using TDD, we first need a framework in which to develop. What I do (using the information on programming tips from Unit 8 and 9), is to start with a working project, and strip it down. In this case, I create a directory structure much like that from the Unit 8 example: fibonacci fibonacci/source fibonacci/source/fibonacci
fibonacci/source/fibonaccitest Into fibonacci/source I place a copy of build.xml, borrowed from another working project (in this case, Unit8). I examine and modify the build.xml file so that it will work with the new project. In this case, all I need to do is search and replace all occurrences of unit8 with fibonacci. At this time I also modify the file packages, making the same substitution so that javadoc will work for this project. Next, I require the one basic Unit test file, AllTests.java. This is the starting point for all tests, so I copy it from Unit8 as well, and modify it such that any references to Unit8 are changed to fibonacci. I also remove all of the suite.addtest( ); lines from the Test method, since I have no test suites yet. This seems both too easy, and rather silly but the point here is to not re-invent the wheel. Taking working code and modifying it to suit a new purpose is perfectly good programming practice. The benefit of this approach comes when you run ant build with this new project, and everything works. Of course, the only file to comile was AllTests.java, but it compiles and runs without error so you have a working clean slate from which to begin TDD. First, we require a test suite for our Fibonacci tests, so I add the line suite.addtest(fibonaccitest.suite()); to the Test method of AllTests.java. If I build this, the Unit tests will fail as I have no class FibonacciTest yet defined. We can create FibonacciTest.java from scratch, or copy it from unit8/unit8tests. I d copy and modify MyGeometryTest.java by renaming it FibonacciTest.java and then removing all the code inside the methods that does not pertain to an empty test suite. Here s what you get: / Title: FibonacciTest Description: JUnit Test Object Copyright: Copyright (c) 2003-2004, Richard S. Huntrods Author: Richard S. Huntrods Version: 1.0 Date: December 9, 2003 @author Richard S. Huntrods @version 1.0 / / Fibonacci public interface / package fibonaccitest; import fibonacci.; import junit.framework.; import java.util.vector; public class FibonacciTest extends TestCase {
public FibonacciTest(String name) { super(name); public static Test suite() { return new TestSuite(FibonacciTest.class); public static void main(string[] args) { System.out.println("Executing FibonacciTest suite"); junit.textui.testrunner.run(suite()); protected void setup() throws Exception { System.out.println(" FibonacciTest Begin"); protected void teardown() throws Exception { System.out.println(" FibonacciTest End"); public void testconstructors() { public void testmutators() { Pretty bare, isn t it? However, if we run ant build, the Unit tests come up green (0 errors, 0 failures) once again. Progress! OOPS! I spoke too soon. We have a compile error. It seems the statement import fibonacci.; in our FibonacciTest class won t compile because the fibonacci/source/fibonacci directory is empty. There are two solutions. Either we can comment out this line, or we can build an empty Fibonacci class in our fibonacci/source/fibonacci directory. Since we will require the Fibonacci class in moments anyway, let s build it now. This time, I ll build it from scratch as it will be so simple: / Title: Fibonacci Description: TME2 Geometry class Copyright: Copyright (c) 2003-2004, Richard S. Huntrods Author: Richard S. Huntrods Version: 1.0 Date: December 9, 2003 @author Richard S. Huntrods @version 1.0 / package fibonacci; public class Fibonacci { Now if we run ant build, the test results are green. Now we do have progress!
Take a moment to examine the javadocs that were automatically build during the build. It s nice to have this automated. Also notice the test output says 2 tests run, even though we didn t actually do anything. Where did these two tests come from? If you re-examine FibonacciTest, notice the two empty methods testconstructors() and testmutators(), which I didn t remove from the Unit8 copy (because we may still want to test the Fibonacci constructors and any mutators). These empty methods, whose names start with test are automatically turned into tests by the JUnit framework. You can remove them if you don t like seeing these empty methods. Now for some real work. Let s create a method testfibonacci inside the FibonacciTest class. Rather than start with an empty method, lets put our first test there. We know that Fibonacci(0) = 1 by definition, so let s test that. Here s the new method: public void testfibonacci() { assertequals("testfibonacci Fibonacci.calculate(0) = 1",1, Fibonacci.calculate(0)); A couple of points. You are the designer here, so you can call this method anything you like. Some suggestions are fibonacci, getfibonacci, calculate, number. You decide. Convention dictates that methods which calculate some value and return it to a calling method are usually declared static (so that you don t require and instance variable), and so I ve implied that here by the way I have called calculate. If you try and build this, the build fails due to compiler errors the calculate method is missing. Now is a good time to establish your TDD environment. That is, in your IDE or editor, you want a window open to build.log, FibonacciTest.java and Fibonacci.java. With these three windows, you can examine and modify everything necessary to complete this TDD example. We start by adding a calculate method. It must return an integer, so have it return 0. Build, and the tests are red. Fix that by having calculate return 1. Now the tests are green. The process for TDD is always the same: Test, Fix, Refactor. First you write the test (that should fail). Next, you write the minimum code to make the test pass. Finally, you refactor that code to make it elegant and simple. By running the tests after every step, you always know the code is correct. This gives you the confidence to continue. Let s do that. Next, we want to test Fib(1) = 1. We add the test to our method: assertequals("testfibonacci Fibonacci.calculate(1) = 1",1, Fibonacci.calculate(1)); The tests are green! Why is this? Well, returning 1 from our calculate() method happens to be the correct answer for both tests. Next test: Fib(2) = 2. Add the test, and it fails. What s the simplest fix? Add an if-else condition to the calculate method: if(number < 2) { else { return 2;
Again, tests are green. But, is it time to refactor? Examining the code in Fibonacci, probably not. It s still pretty simple, and there s no more elegant solution to the current tests. Another point to consider. This process seems pretty boring for such a simple result. Why not jump ahead and program more tests at once? The answer is twofold. First, jumping ahead is often a very good way to get into trouble. If you add several tests and one fails, you don t always know if one or more tests failed. You may spend time fixing the first failure, only to discover that the fix makes it worse for subsequent tests. Better to fix one thing at a time and then proceed. Second, as you gain experience programming and in TDD, you will find there are times you CAN leap ahead. Other times, you will find you must test/fix/refactor one line at a time. In general, let your confidence (and the green tests) be a guide. If things are going well, and you feel comfortable taking bigger steps, then do it. However, if you get an unexpected red light, don t be afraid to comment out (or remove) all the new stuff since the last green light and take smaller steps. Next test: Fib(3) = 3. To start, we fix the red light with another if-else. However, it looks like (from where we stand in these tests) that Fib(0) returns 1, but the others return the input number. Let s refactor our code from this: if(number < 3) { if(number < 2) { else { return 2; else { return 3; (which is getting pretty ugly), to this: if(number <= 1) { else { return number; This is much more elegant, and still gives us the green light on all tests. (NOTE: if(number <= 0) also works which you choose is entirely up to you). Again, you are saying but this is WRONG, and in the grand scheme of things (and Fibonacci numbers), you are correct. However, this is and EXAMPLE, and the purpose of this example is to illustrate the TDD process using something you understand (so you can follow the process clearly). In fact, for the tests we ve built so far, this code is 100% correct! But, we re about to break that. Next test: Fib(4) = 5. Now the tests are red, and the fix is to again put an if statement in the code. However, as we refactor, we realize that there is no simple solution as before. Instead, we must examine the input and output. It seems as if the number we output is really the sum of the two previous output numbers. Let s write this out as a table to see:
Fib(0) = 1 (seems to be a defined output to begin the series) Fib(1) = 1 (also seems to be defined) Fib(2) = 1 + 1 = 2 Fib(3) = 2 + 1 = 3 Fib(4) = 3 + 2 = 5 Fib(n) = Fib(n-1) + Fib(n-2)??? (Of course we know this is the true algorithm for Fibonacci numbers) Lets try this with numbers up to Fib(8) and see. Fib(5) = Fib(4) + Fib(3) = 5 + 3 = 8? correct Fib(6) = Fib(5) + Fib(4) = 8 + 5 = 13 Fib(7) = Fib(6) + Fib(5) = 13 + 8 = 21 Fib(8) = Fib(7) + Fib(6) = 21 + 13 = 34 Indeed, this seems to be the correct algorithm. We can write tests for these numbers, but then we need to convert the algorithm into code to fix the tests. Rather than fix the tests with a series of if statements (or a switch), let s make that leap to code the algorithm By the way, notice that you added four new tests (Fib 5-8), but only get one error. The test framework stops on the first error in any test method. That s why it s important to make small steps until you get comfortable. You only get to see the first error or failure. As it turns out, Java frees us from a terrible burden of programming, as it allows recursive method calls. As a result, we can write this code: if(number <= 1) { else { return calculate(number-1) + calculate(number-2); As it happens, this is the correct code for the algorithm, and (thanks to recursion) happens to be pretty elegant and simple as well! Add a few more tests, confirm the green light and we re DONE. The completed example can be found in the TDD Fibonacci Example link. References Beck, Kent, Test-Driven Development, 2003, Addison Wesley.