Runtime-Bound Test Doubles
9.4 Verifying Output with a Spy
if (buffer == 0) return;
free(buffer);
buffer = 0;
}
int FormatOutputSpy(const char * format, ...) {
int written_size;
va_list arguments;
va_start(arguments, format);
written_size = vsnprintf(buffer + buffer_offset,
buffer_size - buffer_used, format, arguments);
buffer_offset += written_size;
buffer_used += written_size;
va_end(arguments);
return 1;
}
const char * FormatOutputSpy_GetOutput(void) {
return buffer;
}
In the next section, we’ll test-drive some production code that has to print something, using the spy to verify printed output.
9.4 Verifying Output with a Spy
In this section, we’ll look at a utility module, the CircularBuffer. A Circu-larBuffer can be created with a specified capacity; it can have integers added to it and removed from it. It behaves as a first-in first-out data structure. Some of the situations aCircularBuffermight find itself in are shown in Figure9.1, on the next page.
TheCircularBuffer also must print itself out, oldest entry to newest, and not disturb the contents of the buffer. Given all the special cases, this is not quite as easy as it sounds. We’re coming into this problem with a workingCircularBuffer and have to add the print capability. To get our test fixture in place, let’s start with the simplest case, printing an empty buffer. When an empty buffer is printed, its output would look like this:
⇒ Circular buffer content:
<>
VERIFYINGOUTPUT WITH ASPY 187
-2 16 23 7 66 12 99 16 90 99 43 17 13
In-Index Out-Index
41 59 14 33 7 31
In-Index Out-Index
Out-Index In-Index
23 7 66 12 99 16 90
Out-Index In-Index
42 -6 23 7 66 12 99 16 90
Out-Index In-Index
99
Figure 9.1:CircularBuffer
Report erratum this copy is (P1.0 printing, April, 2011)
Download from Wow! eBook <www.wowebook.com>
VERIFYINGOUTPUT WITH ASPY 188
It seems like too simple of a case to test, but it is a boundary test that should not crash the system, and the simple case helps get the test fixture set up properly. Here is the test case that confirms that printing an emptyCircularBufferproduces the desired output:
Download tests/util/CircularBufferPrintTest.cpp
TEST(CircularBufferPrint, PrintEmpty) {
expectedOutput = "Circular buffer content:\n<>\n"; CircularBuffer_Print(buffer);
STRCMP_EQUAL(expectedOutput, actualOutput);
}
TheTEST_GROUPthat supportsCircularBufferprints tests looks like this:
Download tests/util/CircularBufferPrintTest.cpp
TEST_GROUP(CircularBufferPrint) {
CircularBuffer buffer;
const char * expectedOutput;
const char * actualOutput;
void setup() {
UT_PTR_SET(FormatOutput, FormatOutputSpy);
FormatOutputSpy_Create(100);
actualOutput = FormatOutputSpy_GetOutput();
buffer = CircularBuffer_Create(10);
}
void teardown() {
CircularBuffer_Destroy(buffer);
FormatOutputSpy_Destroy();
} };
Look at how actualOutput is assigned during setup( ) before the output has been captured. You might wonder, how can actualOutput be ini-tialized now? FormatOutputSpy_GetOuput( ) simply returns a pointer to the beginning of its internal array that will hold the captured char-acters once the production code print function is called. The Circular-Buffer_Create( ) parameter,10, specifies the capacity of the buffer.
This TEST_GROUP tests just the print function of CircularBuffer. There is anotherTEST_GROUP(not shown) that drove the development of the other
VERIFYINGOUTPUT WITH ASPY 189
CircularBuffer operations. TEST_GROUP(CircularBufferPrint) is an example of organizing tests around a common setup.
This next test is another boundary test. It checks that a buffer con-taining a single item prints properly. When the buffer contains only the number17, the output looks like this:
⇒ Circular buffer content:
<17>
Here is the test that locks in that behavior:
Download tests/util/CircularBufferPrintTest.cpp
TEST(CircularBufferPrint, PrintAfterOneIsPut) {
expectedOutput = "Circular buffer content:\n<17>\n";
CircularBuffer_Put(buffer, 17);
CircularBuffer_Print(buffer);
STRCMP_EQUAL(expectedOutput, actualOutput);
}
This test handles the case when there are a few items in the buffer, but it is not full and has not wrapped around to the first slot. Given that there are the values 10, 20, and 30 in the buffer, the output looks like this:
⇒ Circular buffer content:
<10, 20, 30>
TheTEST( ) looks like this:
Download tests/util/CircularBufferPrintTest.cpp
TEST(CircularBufferPrint, PrintNotYetWrappedOrFull) {
expectedOutput = "Circular buffer content:\n<10, 20, 30>\n";
CircularBuffer_Put(buffer, 10);
CircularBuffer_Put(buffer, 20);
CircularBuffer_Put(buffer, 30);
CircularBuffer_Print(buffer);
STRCMP_EQUAL(expectedOutput, actualOutput);
}
Here is the test for another boundary case: the buffer is completely full, created with a capacity of five, but has not yet wrapped back around to the first location.
Report erratum this copy is (P1.0 printing, April, 2011)
Download from Wow! eBook <www.wowebook.com>
VERIFYINGOUTPUT WITH ASPY 190
Download tests/util/CircularBufferPrintTest.cpp
TEST(CircularBufferPrint, PrintNotYetWrappedAndIsFull) {
expectedOutput = "Circular buffer content:\n"
"<31, 41, 59, 26, 53>\n";
CircularBuffer b = CircularBuffer_Create(5);
CircularBuffer_Put(b, 31);
CircularBuffer_Put(b, 41);
CircularBuffer_Put(b, 59);
CircularBuffer_Put(b, 26);
CircularBuffer_Put(b, 53);
CircularBuffer_Print(b);
STRCMP_EQUAL(expectedOutput, actualOutput);
CircularBuffer_Destroy(b);
}
This test deals with the situation after wrap-around has occurred:
Download tests/util/CircularBufferPrintTest.cpp
TEST(CircularBufferPrint, PrintOldToNewWhenWrappedAndFull) {
expectedOutput =
"Circular buffer content:\n"
"<201, 202, 203, 204, 999>\n";
CircularBuffer b = CircularBuffer_Create(5);
CircularBuffer_Put(b, 200);
CircularBuffer_Put(b, 201);
CircularBuffer_Put(b, 202);
CircularBuffer_Put(b, 203);
CircularBuffer_Put(b, 204);
CircularBuffer_Get(b);
CircularBuffer_Put(b, 999);
CircularBuffer_Print(b);
STRCMP_EQUAL(expectedOutput, actualOutput);
CircularBuffer_Destroy(b);
}
We could write some more tests, but I think you have the picture.
WHEREARE WE? 191
9.5 Where Are We?
We’ve seen a couple applications of the function pointer in making testable C code. First we saw that we use a function pointer to dynami-cally swap out troublesome code for some test cases as we did with the RandomMinute. Then we saw how to surgically insert a test double to substitute only one part of a compilation unit.
Any function call dependency can be converted to a function pointer.
Maybe you wonder, is testability reason enough to convert a direct func-tion call to a funcfunc-tion pointer? Yes, it is. Should you turn all your direct function calls into function pointers? Of course not.
Like any tool, you want to use it when it’s called for. If a link-time test double will do, use a link-time test double. It is the right tool for swap-ping out target platform dependencies and sometimes entire third-party libraries. The function pointer provides a more surgical tool for break-ing dependencies at runtime. If you need the code in the test executable but you also need to get it out of the way for some tests, use a function pointer.
A function pointer is also a good tool for the job if you only want to stub out a subset of the functions in a compilation unit. But it’s not the only tool. You could also split the compilation unit and use link-time binding. Choose the right tool for the job.
We’re not done with function pointers. We employ them not only to make code more testable, but they can also be used to make code more flexible, as you will see when we get into the Chapter11, SOLID, Flexi-ble, and Testable Designs, on page219.
Prior to theFormatOutputSpy, the test doubles have been pretty simple.
There was only a single interaction with the test double during the test case. The FormatOutputSpy could record what happened over multiple interactions. In the next chapter, we’ll look at the mock object. It lets the tests model more complex interactions between collaborating modules.
Put the Knowledge to Work
1. Extend theCircularBufferso that it can print multiple lines of values in neat columns, allowing no more than sixty characters per line.
It needs to handle only five-digit decimal numbers.
Report erratum this copy is (P1.0 printing, April, 2011)
Download from Wow! eBook <www.wowebook.com>
WHEREARE WE? 192
2. Evolve CircularBuffer print capabilities so that the column width adjusts to the two characters wider than the largest number in the buffer.
3. The customer can’t decide how they want theCircularBufferoutput.
Separate print formatting from the CircularBuffer. A PrintFormatter function should be passed to the CircularBuffer, which gives each value in its correct order to the formatter.
4. The randomization implementation and tests for theLightScheduler that you can find in book’s code download in directory code/t0 have just been barely started. Make a test list for the special cases, and test-drive the complete behavior. Two things to consider are that a random scheduled event should be operated only once a day, and a random scheduled event near midnight should handle the day change correctly.
Let’s mock the midnight bell.
William Shakespeare