• No results found

Surgically Inserted Spy

In document Test.driven.development.for.Embedded.C (Page 182-186)

Runtime-Bound Test Doubles

9.3 Surgically Inserted Spy

if (id == LIGHT_ID_UNKNOWN) {

LONGS_EQUAL(id, LightControllerSpy_GetLastId());

LONGS_EQUAL(level, LightControllerSpy_GetLastState());

} else

LONGS_EQUAL(level, LightControllerSpy_GetLightState(id));

}

void setTimeTo(int day, int minute) {

FakeTimeService_SetDay(day);

FakeTimeService_SetMinute(minute);

} };

With function pointers, tests can intercept outgoing function calls from the code under test. It can be a very effective way to surgically isolate calls to particular functions. I’ve also seen this overused, where all of a sudden all functions are converted to function pointers. Use this, but only when it is called for. Like usual, it’s a judgment call.

The code calling a function through a pointer better know it is calling through a pointer. This is easy enough to do; the calling code must see the function’s declaration—if it hasn’t, the compiler will issue a warn-ing and make some assumptions. (You might not notice the warnwarn-ing if you tolerate warnings in your code.1) A call to a previously undeclared function is assumed to be a direct call. Calling a function pointer as a direct call is trouble. You can only hope it fails fast.

The function pointer allows very fine control over what is stubbed and what is not. Let’s see how a single function can be faked out as we look at testing code with printed output.

9.3 Surgically Inserted Spy

When a system has printed output, it is usually manually inspected.

Printed output can be very tedious to verify, so you probably don’t want to reinspect the output as often as you should. We will never get totally away from manually inspecting printed output, but we can eliminate the re-inspections by locking in the desired behavior.

1. Shame on you :-)

SURGICALLYINSER TEDSPY 183

Let’s say you already have aprintf( )-like function to produce printed out-put called FormatOutput( ). As it is, FormatOutput( ) is a direct function call that is in a header file with many other utilities. You’d like to create a spy for FormatOutput( ), but you don’t want to stub out all the func-tions in the file containingFormatOutput( ). A more surgical approach is needed to intercept calls to just one function in a compilation unit. The prototype for the direct function call toFormatOutput( ) looks like this:

Download include/util/Utils.h

int FormatOutput(const char *, ...);

A function pointer is the right tool for surgically intercepting calls to FormatOutput( ). To infiltrate the callers of FormatOutput( ), first convert theFormatOutput( ) prototype to a function pointer.

Download include/util/Utils.h

extern int (*FormatOutput)(const char *, ...);

In the .cfile, renameFormatOutput( ) toFormatOutput_Impl( ). Then create the defining instance of the FormatOutput function pointer, initializing it to the address ofFormatOutput_Impl( ). To keep outsiders from calling FormatOutput_Impl( ) directly, you should also make itstatic:

Download src/util/Utils.c

static int FormatOutput_Impl(const char * format, ...) {

/* snip */

}

int (*FormatOutput)(const char * format, ...) = FormatOutput_Impl;

With those changes to the .h and .cfiles, rebuild. There is no need to change the callers ofFormatOutput( ), because the calling syntax is the same for a direct call and a call through a function pointer.

If you are usingprintf( ) directly, you can do the same thing, initializing FormatOutput_Impl( ) like this:

Download src/util/Utils.c

int (*FormatOutput)(const char * format, ...) = printf;

If your make does not use #include dependencies to decide what to rebuild, do a clean build. (Later invest in your build to do a proper incremental build based on dependencies.) If all callers to FormatOut-put( ) are not recompiled, they will call the function pointer as a direct call; bad things will happen.

Report erratum this copy is (P1.0 printing, April, 2011)

Download from Wow! eBook <www.wowebook.com>

SURGICALLYINSER TEDSPY 184

Let’s write a test that shows how to use a FormatOutputSpy. The For-matOutputSpycaptures whatever was to be printed so it can be retrieved and checked in the test case.

Download mocks/FormatOutputSpyTest.cpp

TEST(FormatOutputSpy, HelloWorld) {

FormatOutputSpy_Create(20);

FormatOutput("Hello, World\n");

STRCMP_EQUAL("Hello, World\n", FormatOutputSpy_GetOutput());

}

When the spy is created, it is told how long of a string to capture. When calls toFormatOutput( ) have been overridden withFormatOutputSpy( ), the output can be accessed by a call toFormatOutputSpy_GetOuput( ).

TheTEST_GROUP( ) in the next code segment is responsible for overriding FormatOutput( ) and cleaning up after each test:

Download mocks/FormatOutputSpyTest.cpp

extern "C"

{

#include "FormatOutputSpy.h"

}

TEST_GROUP(FormatOutputSpy) {

void setup() {

UT_PTR_SET(FormatOutput, FormatOutputSpy);

}

void teardown() {

FormatOutputSpy_Destroy();

} };

This test illustrates that the spy only captures the number of characters specified inFormatOutputSpy_Create( ):

Download mocks/FormatOutputSpyTest.cpp

TEST(FormatOutputSpy, LimitTheOutputBufferSize) {

FormatOutputSpy_Create(4);

FormatOutput("Hello, World\n");

STRCMP_EQUAL("Hell", FormatOutputSpy_GetOutput());

}

SURGICALLYINSER TEDSPY 185

Like realFormatOutput( ), the spy can be called multiple times. This test shows that the spy appends characters with eachFormatOutput( ) call:

Download mocks/FormatOutputSpyTest.cpp

TEST(FormatOutputSpy, PrintMultipleTimes) {

FormatOutputSpy_Create(25);

FormatOutput("Hello");

FormatOutput(", World\n");

STRCMP_EQUAL("Hello, World\n", FormatOutputSpy_GetOutput());

}

In this final test, the spy is called multiple times with more output than the spy can capture. This test assures that the output captured is limited to the specified maximum string length.

Download mocks/FormatOutputSpyTest.cpp

STRCMP_EQUAL("1234567890AB", FormatOutputSpy_GetOutput());

}

Not all test doubles need tests. But in this case, with a more complex spy, tests are needed. The tests show how the spy behaves and makes sure it works. Here are the inner workings of the spy:

Download mocks/FormatOutputSpy.c

#include <stdlib.h>

#include <stdarg.h>

static char * buffer = 0;

static size_t buffer_size = 0;

static int buffer_offset = 0;

static int buffer_used = 0;

void FormatOutputSpy_Create(int size) {

FormatOutputSpy_Destroy();

buffer_size = size+1;

buffer = (char *)calloc(buffer_size, sizeof(char));

buffer_offset = 0;

buffer_used = 0;

buffer[0] = '\0';

}

Report erratum this copy is (P1.0 printing, April, 2011)

Download from Wow! eBook <www.wowebook.com>

VERIFYINGOUTPUT WITH ASPY 186

void FormatOutputSpy_Destroy(void) {

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.

In document Test.driven.development.for.Embedded.C (Page 182-186)