I have a home-grown unit testing framework for C programs on Linux using GCC. For each file in the project, let's say foobar.c
, a matching file foobar-test.c
may exist. If that is the case, both files are compiled and statically linked together into a small executable foobar-test
which is then run. foobar-test.c
is expected to contain main()
which calls all the unit test cases defined in foobar-test.c
.
Let's say I want to add a new test file barbaz-test.c
to exercise sort()
inside an existing production file barbaz.c
:
// barbaz.c
#include "barbaz.h"
#include "log.h" // declares log() as a linking dependency coming from elsewhere
int func1() { ... res = log(); ...}
int func2() {... res = log(); ...}
int sort() {...}
Besides sort()
there are several other functions in the same file which call into log()
defined elsewhere in the project.
The functionality of sort()
does not depend on log()
, so testing it will never reach log()
. Neither func1()
nor func2()
require testing and won't be reachable from the new test case I am about to prepare.
However, the barbaz-test
executable cannot be successfully linked until I provide stub implementations of all dependencies coming from barbaz.c
. A usual stub looks like this:
// in barbaz-test.c
#include "barbaz.h"
#include "log.h"
int log() {
assert(false && "stub must not be reached");
return 0;
}
// Actual test case for sort() starts here
...
If barbaz.c
is large (which is often the case for legacy code written with no regard to the possibility to test it), it will contain many linking dependencies. I cannot start writing a test case for sort()
until I provide stubs for all of them. Additionally, it creates a burden of maintaining these stubs, i.e. updating their prototypes whenever the production counterpart is updated, not forgetting to delete stubs which no longer are required etc.
What I am looking for is an option to have late runtime binding performed for missing symbols, similarly to how it is done in dynamic languages, but for C. If an unresolved symbol is reached during the test execution, that should lead to a failure. Having a proper diagnostic about the reason would be ideal, but a simple NULL pointer dereference would be good enough.
My current solution is to automate the initial generation of source code of stubs. It is done by analyzing of linking error messages and then looking up declarations for missing symbols in the headers. It is done in an ad-hoc manner, e.g. it involves "parsing" of C code with regular expressions.
Needless to say, it is very fragile: depends on specific format of linker error messages and uniformly formatted function declarations for regexps to recognize. It does not solve the future maintenance burden such stubs create either.
Another approach is to collect stubs for the most "popular" linking dependencies into a common object file which is then always linked into the test executables. This leaves a shorter list of "unique" dependencies requiring attention for each new file. This approach breaks down when a slightly specialized version of a common stub function has to be prepared. In such cases linking would fail with "the same symbol defined twice".