Doing C code unit testing on a shoestring: Part 3 - Building a unit test framework
Limitations of the approachThe 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).


Loading comments... Write a comment