Doing C code unit testing on a shoestring: Part 1- The basics and the tools

Ark Khasin, MacroExpressions

November 20, 2007

Ark Khasin, MacroExpressions

Test Instrumentation
Creating instrumented code out of your production code comprises an intimidating amount of menial labor. It is here where the test automation tools ought to shine. They do, provided they parse your code and (important!) its dependencies correctly.

However, a lot of automation can be accomplished by using clever C macros. To get a taste of it, consider a definition in file scope (i.e., outside of any block)

static short foo;

The problem is to inspect its value, say, before and after executing a test case, without modifying your source file. While this seems impossible, we can do this if we have a macro like this:

#define static /*nothing*/

Note that it is a valid C macro which makes foo a variable with external linkage. Your harness code can now say:

extern short foo;

This trick won't work though for static objects in a block scope: the macro will make foo an automatic. This can be repaired by a variation on the theme:

#define static extern

The new macro turns a definition into a declaration, and it turns out that it is legal in C to put declarations any place you could put a definition. The definition itself,

short foo;

will then go to your test harness file.

Note: This technique won't work as expected if there are several foo in several blocks, whether nested or not, as in

static short foo;
if (x)
{
        static short foo;
        ...............
        if (y)
        {
        static short foo;
        ...............
    }
}
else
{
        static short foo;
        ...............
}

Few corporate coding standards, especially in safety-related environment, would tolerate this style better fit for the Obfuscated C Contest. So we can safely disregard this drawback.

Other instrumentation techniques will be demonstrated in the next section. Our concern now will be, where to put the macro responsible for the trick

#define static extern

We do not want this definition visible anywhere except in the unit under test. To achieve this goal, let's do the following.

Assume that there is a header file in your project that is included in every source file. It is more than likely that you have one already; it might hold global project configuration parameters and/or include common goodies like stddef.h and limits.h. Let's say it is called project.h, so all sources have a statement

#include "project.h"

Let's now modify this ubiquitous project.h by adding the following:

#include "instrum.h"

This can be treated by project developers as a magic incantation; the header "instrum.h" shall have no effect on the normal build process. However, it is responsible for creating instrumentation when a source becomes the unit under test.

To achieve this variable behavior, we construct "instrum.h" to begin with the following passage:

#ifdef INSTRUM_HEADER
#     include INSTRUM_HEADER
#endif /*INSTRUM_HEADER*/

The idea is that in normal compilation of a source file, INSTRUM_HEADER is not defined and the source compiles as it always did.

When, however, a source file, say foo.c, is the UUT, we create an instrumentation header file for it, say, instrum_foo.h and pass the definition

INSTRUM_HEADER="instrum_foo.h"

on the command line of the compiler. (Usually, it's a "D compiler switch, or an equivalent configuration in the integrated development environment. Note that on Windows platforms passing a quoted definition in the command line is a tricky dealing with CMD.EXE; try

INSTRUM_HEADER="\"instrum_foo.h\"

As we will see later, there is a good chance to use the same instrumentation header for all sources to be unit-tested, say, instrum_common.h. This approach will be our target, and the definition to pass to the compiler is

INSTRUM_HEADER="instrum_common.h"

The instrumentation header, instrum_common.h, will have

#define INSTRUM_STATIC extern

(Or it may have #define INSTRUM_STATIC or no definition for INSTRUM_STATIC at all, depending on the needs.)

The "magic" header, instrum.h, will have the following:

#ifdef INSTRUM_STATIC
#     define static INSTRUM_STATIC
#endif /*INSTRUM_STATIC*/

That is, if instrumentation for static is not defined, the keyword will keep its normal meaning. Otherwise, its meaning becomes whatever the instrumentation header assigns to it; this remains completely transparent to the magic header.

C code instrumentation
Now we are in a position to devise instrumentation of the code by abusing other keywords.

Instrumenting the if statements. Following the pattern outlined in the previous section, let's add the following to instrum.h:

#ifdef INSTRUM_IF
#     define INSTRUM_IF(condition)
#endif

The instrumentation header instrum_common.h would have something like

#define INSTRUM_IF(condition) \
        if (instrum_if(#condition, (condition)!=0, \
        __FILE__, __LINE__, __FUNCTION__))

extern int instrum_if(const char *condition_name,
                                    int condition,
                                    const char *filename,
                                    int line,
                                    const char *function_name);

Implementation of instrum_if can be anything you want it to be, except that in order not to alter the behavior of you code, it must return its second argument (condition). For instance, the following implementation just prints what condition where and if it is true or false:

int instrum_if(const char *condition_name,
                        int condition,
                        const char *filename,
                        int line,
                        const char *function_name)
{
    printf("Condition %s in funtion %s"
            " (file %s line %d) is %s\n",
            condition_name, function_name,
            filename, line, (condition)?"true":"false");
    return condition;
}

This implementation goes to some instrumentation support source file. Of course this file itself should not be instrumented.

Some (pre-C99) compilers might not define __FUNCTION__, or you might not care to output the file name. Your implementation might be like so:

#define INSTRUM_IF(condition) \
        if(instrum_if1(#condition, (condition)!=0, \
        __LINE__))

extern int instrum_if1(const char *condition_name,
                                    int condition,
                                    int line);

with a corresponding implementation.

We will see later that instrumentation of other keywords will require a use of if. The if will come there instrumented but we don't want to see the effects of that instrumentation. So a real instrumentation function for if must detect whether the if is a result of instrumentation of something else and if so suppress all instrumentation actions.

To read Part 2, go to "Toward code coverage analysis."
To read Part 3, go to "Building a unit test framework."

Ark Khasin, PhD, is with MacroExpressions. He can be contacted at akhasin@macroexpressions.com.

< Previous
Page 2 of 2
Next >

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER