Doing C code unit testing on a shoestring: Part 3 - Building a unit test framework

Ark Khasin, MacroExpressions

September 25, 2007

Ark Khasin, MacroExpressions

Limitations of the approach
The instrumentation techniques for code coverage analysis are not bullet-proof: they require that an announcement of a statement uniquely identifies it. A simple example of where it is not the case is a construct like

if(++x) a; else if(++x) ...

where it is not easy to come up with instrumentation of if which would ensure a unique identification of each if statement.

Normally, these cases can be addressed by the coding policy. (E.g., MISRA wants a block to follow an if and a coding style usually wants a newline to precede or to follow an opening curly brace.)

There are rare cases though where nested blocks with ifs and loop and switch constructs comprise a macro definition, and occasionally such a macro has merits. When such a macro is expanded, all instrumentation functions will get the same __FILE__, __LINE__, and __FUNCTION__ values, so unique identification may be tricky.

Secondly, there is no transparent way to instrument the ternary operator, which we conveniently ignored previously. For instance,

if (x) {y=u;} else {y=v;}

has the same meaning and result as

y=(x)?u:v;

The former case can be instrumented and analyzed whereas the latter cannot. A workaround lies in the coding policy: One can require to use the ternary operator in all non-constant expressions with the ISTRUE macro (discussed with the for instrumentation), like so:

y=ISTRUE(x)?u:v;

Remarks on C++ and abnormal execution paths
So far, we've been discussing the normal control flow. The C language allows only one exceptional control flow mechanism, namely, setjmp/longjmp. There is nothing special to be done to account for it since it appears as normal control flow based on the return value of setjmp.

The try/throw/catch exception mechanism of C++ is a little harder to deal with. We need to analyze whether each catch statement had been hit during the test set execution. This can be done with the same trick we used (effectively) for the for statement instrumentation:

#define catch(a) catch(a) if(instrum_catch()) {} else

The implementation of instrum_catch() is expected to always return false and to never throw.

In the introduction to this series ( Part 1), I conceded upfront that proof of coverage in the presence of overloaded or overridden functions can be problematic.

The reason is that we don't necessarily know what functions have been executed. However, in the object oriented (OO) paradigm, that's not a problem at all: it's precisely the purpose of the design that the unit doesn't know what it is calling as long as the interface is provided.

Thus, a test of the unit should only establish that any suitable implementation has been called. In this view, a stub (e.g., from a derived class) that announces its own execution is sufficient (and the concession in the abstract withdrawn).

< Previous
Page 2 of 3
Next >

Loading comments...

Most Commented

Parts Search Datasheets.com

KNOWLEDGE CENTER