We compile and run our tests again, expecting success. This time we are (somewhat) surprised, as we receive the same error again.
After scratching our heads for a few seconds, we suspect that the static total is probably not getting initialized. We probe for an answer via our tests (not the debugger):
TEST( Sale, sellTwoItems ) { CHECK( 0 == GetTotal( ) ); BuyItem( milkBarcode ); BuyItem( milkBarcode ); CHECK( 2 * milkPrice == GetTotal( ) ); }
Aha. This time we receive two failures. The first one confirms our suspicion total
is not initialized at the start of sellTwoItems
:
Failure: "0 == GetTotal( )" line 27 in SaleTest.cpp Failure: "2 * milkPrice == GetTotal( )" line 30 in SaleTest.cpp
We just love that rapid feedback that TDD gives us!
The way the test harness works is that each TEST
is supposed to be independent of one another. In fact, you cannot guarantee that the tests will run in any particular order. (This actually introduced some problems for us see [2].) Each test needs to build its own setup.
We hate day-long debugging sessions that result when one test is unwittingly dealing with the remnants of some previous test. However, if you follow the rule of writing just enough code to make tests pass, you will be forced to add more tests. Usually these additional tests will uncover such insidious problems.
For the Sale
example, we wrote just enough code to get it to work for one item and one item only. That forced us to write a test for selling two items, which exposed our error. To fix the problem, we introduced an initialization function that each test and therefore each client that will use Sale
will have to call.
static char* milkBarcode = "a123"; static int milkPrice = 199; TEST( Sale, totalNewSale ) { Initialize( ); CHECK( 0 == GetTotal( ) ); } TEST( Sale, sellOneItem ) { Initialize( ); BuyItem( milkBarcode ); CHECK( milkPrice == GetTotal( ) ); } TEST( Sale, sellTwoItems ) { Initialize( ); CHECK( 0 == GetTotal( ) ); BuyItem( milkBarcode ); BuyItem( milkBarcode ); CHECK( 2 * milkPrice == GetTotal( ) ); }
After declaring Initialize
in Sale.h
, we add its definition to Sale.c
:
void Initialize() { total = 0; }
All tests run at this point.
Dealing with Databases
So far, we are only supporting selling one product at a $1.99. We add a fourth test, to ensure that we can deal with selling two products.
TEST( Sale, sellTwoProducts ) { Initialize( ); char* cookieBarcode = "b234"; int cookiePrice = 50; BuyItem( milkBarcode ); BuyItem( cookieBarcode ); CHECK( milkPrice + cookiePrice == GetTotal( ) ); }
We compile and run the tests, which fail:
Failure: "milkPrice + cookiePrice == GetTotal()" line 46 in SaleTest.cpp
There was one failure.
Getting this test to pass is simple:
void BuyItem( char* barCode ) { if ( 0 == strcmp( barCode, "a123" ) ) total += 199; else if ( 0 == strcmp( barCode, "b234" ) ) total += 50; }
We know that we have to do a lookup someplace using the barcode. Since we have passing tests, we can now refactor to do a lookup, all the while ensuring that all our tests still pass. Our refactoring is very incremental: we change only a very small amount of code before getting feedback by running our tests. It is otherwise too easy to introduce a defect while making dramatic code changes.
Our small step for now is to introduce a placeholder for our lookup in the form of a call to a GetPrice
function. The code in BuyItem
will demonstrate what we want it to do. The real details of the how will come later.
void BuyItem( char* barCode ) { int price = GetPrice( barCode ); total += price; }
In other words, our intent is that some other function will do the job of looking up the item price by barcode. This is known as programming by intention. It is a very powerful technique because it forces us to work on smaller and smaller problems, until the how is obvious. For now, we move the conditional logic from BuyItem
to the GetPrice
function:
int GetPrice( char* barCode ) { if ( 0 == strcmp( barCode, "a123" ) ) return 199; else if ( 0 == strcmp( barCode, "b234" ) ) return 50; }
We add the declaration of this function to Sale.h
and run our tests to ensure they still pass.
Now where is GetPrice
actually going to find the price of the item? In the database, of course.
But using a real database for development is a major undertaking. We dont want to go up against a live database, so we have to get a working snapshot. Then well have to track any changes that the DBA makes to the real database to keep our snapshot up to date.
Theres also the performance consideration of working with a database. Wed have to connect to it for each test, something that takes a lot of processing time. Yet we need to run our tests every few minutes. The slowness of establishing the connection would significantly decrease the amount of work we get done each day.
Instead, we want to stub out the calls to the database. All that BuyItem
cares about is that GetPrice
returns the price of the item. It doesnt care if the price comes from a database lookup or if GetPrice
makes up a random price on the spot. We do, of course, have to test the real GetPrice
when we use TDD to build the real database code.
In order for the call to GetPrice
to be able to do both a database lookup in production code, as well as provide the numbers that we want for our tests, we have to access it indirectly. The usual way to get different behavior from a function is to use a function pointer. The test will set the pointer to its own stub lookup function, and the production code will set the pointer to a function that does the live database lookup.
In SaleTest
, we start by supplying a stub function, GetPriceStub
, for the lookup:
int GetPriceStub( char* barCode ) { if ( 0 == strcmp( barCode, "a123" ) ) return 199; else if ( 0 == strcmp( barCode, "b234" ) ) return 50; }
Our tests then need to pass a pointer to this stub function as a parameter to Initialize
. We change the call to Initialize
in each test:
Initialize( &GetPriceStub );
This wont compile. We fix the prototype and definition of Initialize
in Sale.h
and Sale.c
. Its new signature is:
void Initialize( int (*LookUpPrice)(char* barCode) )
Initialize
then needs to store that function pointer. Once again, we use a static
variable declared in Sale.c
.
static int (*LookUpPrice)(char* barCode); void Initialize( int (*db)(char*) ) { total = 0; LookUpPrice = db; }
Finally, GetPrice
is altered so that it dereferences the database function pointer in order to call the database lookup function.
int GetPrice( char* barCode ) { return LookUpPrice( barCode ); }
We build, run tests, and succeed.
Now we can get the price of the item in any way that we choose. For the test, we use a simple if/else construct to return hard-coded prices based on the barcodes we know about. In the production code, whoever constructs the point-of-sale system will have to supply a function pointer to Sale
that accesses the real database. (This function would of course have its own set of tests!)
Link-Time Polymorphism
Pointers are powerful but very dangerous, evidenced by the fact that the major languages introduced in the last decade have moved to eliminate them. The problem with pointers is that we must make sure that they refer to what we intend. It is all too easy for a stray pointer to cause the program to crash, inflicting late-night debugging sessions for the entire development team. Because dereferencing an invalid pointer is a certain bug and probable crash, many programmers test the validity of each pointer prior to dereferencing it. Such checking makes code difficult to read.
There is an alternative to using pointers pointing at different functions: link-time polymorphism. We instead use the linker to link in different functions. The function BuyItem( char* )
will continue to call GetPrice( char* )
, but there will be two GetPrice( char* )
functions defined: one for testing, which well place in the test file SaleTest.cpp
, and another, which well place in a file GetPrice.c
.
When were building for test every few minutes throughout the development day, we link in SaleTest.o
and Sale.o
. When we want to build the real system, we link in the real application and GetPrice.o
, but not SaleTest.o
. Here is a very simplistic Makefile
to illustrate the two builds:
POSApp: Sale.o GetPrice.o main.o g++ -o POSApp Sale.o GetPrice.o main.o SaleTest: SaleTest.o Sale.o g++ -o SaleTest SaleTest.o Sale.o \ ../TestHarness/TestHarness.a SaleTest.o: SaleTest.cpp Sale.h g++ -c SaleTest.cpp Sale.o: Sale.c Sale.h GetPrice.h gcc -c Sale.c GetPrice.o : GetPrice.c GetPrice.h gcc -c GetPrice.c
Conclusion
An extremely important thing to note in this exercise is the very small steps that we took during our development. Our goal was to ensure that we incrementally improved the system at a steady rate. We wanted to see our tests pass every 5 to 10 minutes or less. This kept us from spending too much time going in the wrong direction. At the end of the day, the product does more than it did at the beginning of the day, and it does it correctly.
TDD, to put it mildly, is awesome. We also think it is the most important movement in software development today. We continually meet developers who tell us they have tried it and would never willingly give it up. And as weve demonstrated here, TDD is not just for objects: you can do it in C. In fact, our position is that since C is such a powerful but dangerous language due to its low-level nature, you really must protect yourself with TDD.
The example we chose was admittedly scaled down so that we could illustrate the techniques of TDD. TDD has been proven to scale to large and complex systems. In fact, one of the chief goals of TDD is to ensure that we take simplified approaches to complexity. Properly managing complexity through TDD means that we can build systems that can be maintained over long periods of time at a reasonable cost.
References
[1] A user story is a requirement that: 1) has demonstrable business value, 2) can be tested, and 3) can be completed within an iteration (typically two weeks).
[2] We encountered some interesting results when we coded this Sale
example on a second machine. The tests ran in a different order not top to bottom because the testing framework produces a linked list of static objects, and the order of static initialization isnt guaranteed across compilers and operating systems. In this case, sellOneItem
ran prior to totalNewSale
. The total variable thus held a non-zero amount when totalNewSale
executed, causing it to fail. This would have ended up being slightly earlier feedback to tell us to initialize the variable properly.
Dr. Robert S. Koss is a senior consultant at Object Mentor, Inc. He has been programming for 29 years, the last 15 years in C and C++. Dr. Koss has conducted hundreds of classes in C, C++, and OO Design throughout the world, training thousands of students.
Jeff Langr is a senior consultant at Object Mentor, Inc. He has authored several articles as well as the book Essential Java Style (Prentice Hall, 1999). Langr has over 20 years of software development experience, including 10 years of OO development in C++, Smalltalk, and Java.