Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Test Driven Development in C/C++


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 don’t want to go up against a live database, so we have to get a working snapshot. Then we’ll have to track any changes that the DBA makes to the real database to keep our snapshot up to date.

There’s also the performance consideration of working with a database. We’d 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 doesn’t 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 won’t 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 we’ll place in the test file SaleTest.cpp, and another, which we’ll place in a file GetPrice.c.

When we’re 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 we’ve 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 isn’t 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.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.