Test-Driven Development


Suppose that we followed three simple rules.

  1. Don't write any production code until you have written a failing unit test.

  2. Don't write more of a unit test than is sufficient to fail or fail to compile.

  3. Don't write any more production code than is sufficient to pass the failing test.

If we worked this way, we'd be working in very short cycles. We'd be writing just enough of a unit test to make it fail and then just enough production code to make it pass. We'd be alternating between these steps every minute or two.

The first and most obvious effect is that every single function of the program has tests that verify its operation. This suite of tests acts as a backstop for further development. It tells us whenever we inadvertently break some existing functionality. We can add functions to the program or change the structure of the program without fear that in the process, we will break something important. The tests tell us that the program is still behaving properly. We are thus much freer to make changes and improvements to our program.

A more important but less obvious effect is that the act of writing the test first forces us into a different point of view. We must view the program we are about to write from the vantage point of a caller of that program. Thus, we are immediately concerned with the interface of the program as well as its function. By writing the test first, we design the software to be conveniently callable.

What's more, by writing the test first, we force ourselves to design the program to be testable. Designing the program to be callable and testable is remarkably important. In order to be callable and testable, the software has to be decoupled from its surroundings. Thus, the act of writing tests first forces us to decouple the software!

Another important effect of writing tests first is that the tests act as an invaluable form of documentation. If you want to know how to call a function or create an object, there is a test that shows you. The tests act as a suite of examples that help other programmers figure out how to work with the code. This documentation is compilable and executable. It will stay current. It cannot lie.

Example of Test-First Design

Just for fun, I recently wrote a version of Hunt the Wumpus. This program is a simple adventure game in which the player moves through a cave, trying to kill the Wumpus before being eaten by the Wumpus. The cave is a set of rooms connected by passageways. Each room may have passages to the north, south, east, or west. The player moves about by telling the computer which direction to go.

One of the first tests I wrote for this program was testMove (Listing 4-1). This function created a new WumpusGame, connected room 4 to room 5 via an east passage, placed the player in room 4, issued the command to move east, and then asserted that the player should be in room 5.

Listing 4-1.

[Test] public void TestMove() {   WumpusGame g = new WumpusGame();   g.Connect(4,5,"E");   g.GetPlayerRoom(4);   g.East();   Assert.AreEqual(5, g.GetPlayerRoom()); }

All this code was written before any part of WumpusGame was written. I took Ward Cunningham's advice and wrote the test the way I wanted it to read. I trusted that I could make the test pass by writing the code that conformed to the structure implied by the test. This is called intentional programming. You state your intent in a test before you implement it, making your intent as simple and readable as possible. You trust that this simplicity and clarity points to a good structure for the program.

Programming by intent immediately led me to an interesting design decision. The test makes no use of a Room class. The action of connecting one room to another communicates my intent. I don't seem to need a Room class to facilitate that communication. Instead, I can simply use integers to represent the rooms.

This may seem counterintuitive to you. After all, this program may appear to you to be all about rooms, moving between rooms, finding out what rooms contain, and so on. Is the design implied by my intent flawed because it lacks a Room class?

I could argue that the concept of connections is far more central to the Wumpus game than the concept of room. I could argue that this initial test pointed out a good way to solve the problem. Indeed, I think that is the case, but it is not the point I'm trying to make. The point is that the test illuminated a central design issue at a very early stage. The act of writing tests first is an act of discerning between design decisions.

Note that the test tells you how the program works. Most of us could easily write the four named methods of WumpusGame from this simple specification. We could also name and write the three other direction commands without much trouble. If later we wanted to know how to connect two rooms or move in a particular direction, this test will show us how to do it in no uncertain terms. This test acts as a compilable and executable document that describes the program.

Test Isolation

The act of writing tests before production code often exposes areas in the software that ought to be decoupled. For example, Figure 4-1 shows a simple UML diagram of a payroll application. The Payroll class uses the EmployeeDatabase class to fetch an Employee object, asks the Employee to calculate its pay, passes that pay to the CheckWriter object to produce a check, and, finally, posts the payment to the Employee object and writes the object back to the database.

Figure 4-1. Coupled payroll model


Presume that we haven't written any of this code yet. So far, this diagram is simply sitting on a whiteboard after a quick design session.[1] Now we need to write the tests that specify the behavior of the Payroll object. A number of problems are associated with writing this test. First, what database do we use? Payroll needs to read from some kind of database. Must we write a fully functioning database before we can test the Payroll class? What data do we load into it? Second, how do we verify that the appropriate check got printed? We can't write an automated test that looks on the printer for a check and verifies the amount on it!

[1] [Jeffries2001]

The solution to these problems is to use the MOCK OBJECT pattern.[2] We can insert interfaces between all the collaborators of Payroll and create test stubs that implement these interfaces.

[2] [Mackinnon2000]

Figure 4-2 shows the structure. The Payroll class now uses interfaces to communicate with the EmployeeDatabase, CheckWriter, and Employee. Three MOCK OBJECTs have been created that implement these interfaces. These MOCK OBJECTs are queried by the PayrollTest object to see whether the Payroll object managed them correctly.

Listing 4-2 shows the intent of the test. It creates the appropriate MOCK OBJECTs, passes them to the Payroll object, tells the Payroll object to pay all the employees, and then asks the MOCK OBJECTs to verify that all the checks were written correctly and that all the payments were posted correctly.

Of course, this test is simply checking that Payroll called all the right functions with all the right data. The test is not checking that checks were written or that a true database was properly updated. Rather, it's checking that the Payroll class is behaving as it should in isolation.

Figure 4-2. Decoupled Payroll using MOCK OBJECTS for testing


Listing 4-2. TestPayroll

[Test] public void TestPayroll() {   MockEmployeeDatabase db = new MockEmployeeDatabase();   MockCheckWriter w = new MockCheckWriter();   Payroll p = new Payroll(db, w);   p.PayEmployees();   Assert.IsTrue(w.ChecksWereWrittenCorrectly());   Assert.IsTrue(db.PaymentsWerePostedCorrectly()); }

You might wonder what the MockEmployee is for. It seems feasible that the real Employee class could be used instead of a mock. If that were so, I would have no compunction about using it. In this case, I presumed that the Employee class was more complex than needed to check the function of Payroll.

Serendipitous Decoupling

The decoupling of Payroll is a good thing. It allows us to swap in different databases and checkwriters for both testing and extending of the application. I think it is interesting that this decoupling was driven by the need to test. Apparently, the need to isolate the module under test forces us to decouple in ways that are beneficial to the overall structure of the program. Writing tests before code improves our designs.

A large part of this book is about design principles for managing dependencies. Those principles give you some guidelines and techniques for decoupling classes and packages. You will find these principles most beneficial if you practice them as part of your unit testing strategy. It is the unit tests that will provide much of the impetus and direction for decoupling.




Agile Principles, Patterns, and Practices in C#
Agile Principles, Patterns, and Practices in C#
ISBN: 0131857258
EAN: 2147483647
Year: 2006
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net