It’s day one of adding tests to your legacy C code. You get stopped dead when the compiler announces that the code you are coaxing into the test harness can’t be compiled on this machine. You are stuck on the Make it compile step of Crash to Pass.
Moving your embedded legacy C code (embedded C code without tests) into a test harness can be a challenge. The legacy C code is likely to be tightly bound to the target processor. This might not be a problem for production, but for off-target unit testing, it is a big problem.
For C we have a limited mechanisms for breaking dependencies. In my book, I describe at length link-time and function pointer substitutions, but only touch on preprocessor stubbing.
In this article we’ll look at #include
Test-Double as a way to break dependencies on a problem #include
file.
You have a header file, provided by your silicon vendor, that depends on the target. This is similar to one I ran into last week:
/* * ---- acmetypes.h ---- */ #ifndef _ACME_STD_TYPES #define _ACME_STD_TYPES #if defined(_ACME_X42) typedef unsigned int Uint_32; typedef unsigned short Uint_16; typedef unsigned char Uint_8; typedef int Int_32; typedef short Int_16; typedef char Int_8; #elif defined(_ACME_A12) typedef unsigned long Uint_32; typedef unsigned int Uint_16; typedef unsigned char Uint_8; typedef long Int_32; typedef int Int_16; typedef char Int_8; #else #error <acmetypes.h> is not supported for this environment #endif #endif /* _ACME_STD_TYPES */ |
The C preprocessor stops when it gets to the #error
. You might think you can just define _ACME_X42
or _ACME_A12
, but you are testing off-target, and likely on a 64 bit machine (as I am at the time of this writing), and that can lead to problems where int
size matters.
It is always best to include the production header file, but sometimes that won’t work, especially in legacy situations. We don’t have to give up though. We can employ a #include
Test-Double.
In this case it will be pretty easy. We can define a new header file with the same name, and put it in the include path in front of the production code include. #include
Test-Double looks like this:
/* * acmetypes.h - test double for off target testing */ #ifndef ACMETYPES_H_ #define ACMETYPES_H_ #include <stdint.h> typedef uint32_t Uint_32; typedef uint16_t Uint_16; typedef uint8_t Uint_8; typedef int32_t Int_32; typedef int16_t Int_16; typedef int8_t Int_8; #endif /* ACMETYPES_H_ */ |
Off target you have your development system’s native implementation of stdint.h
. It is the standard way to deal with specific sized int
s when you must. Above we just redefined the Acme types in therms of the portable types.
If you are using CppUTest and its makefile system, you can direct the makefile to look at your test-double header files before the production code header files, like this:
INCLUDE_DIRS =\ .\ include \ include/* \ $(CPPUTEST_HOME)/include/ \ mocks/includes \ $(ACME_INCLUDES) \ |
Point $(ACME_INCLUDES)
at the silicon vendor dependent include directory. Place the test-double header file in some directory like mocks/includes
. As long as mocks/includes
is before $(ACME_INCLUDES)
in the INCLUDE_DIRS
, the test-double will take the place of the production code header file. During the production build mocks/includes
will not be part of the include path.
You don’t need to use CppUTest’s makefile system for this. All compilers I am aware of use the include path approach.
To make sure this all works as we expect, we can also add this test case.
extern "C" { #include "acmetypes.h" } #include "CppUTest/TestHarness.h" TEST_GROUP(acmetypes) {}; TEST(acmetypes, checkIntSizes) { LONGS_EQUAL(1, sizeof(Uint_8)); LONGS_EQUAL(1, sizeof(Int_8)); LONGS_EQUAL(2, sizeof(Uint_16)); LONGS_EQUAL(2, sizeof(Int_16)); LONGS_EQUAL(4, sizeof(Uint_32)); LONGS_EQUAL(4, sizeof(Int_32)); } |
There are other situations where a #include
stub can be used. For example break a long chain of #include
s where you just need a couple symbols. But keep in mind, it is still better to use the production code header when you can.
If this helped, tell me about it. If I’m missing something, tell me that too.
James,
I translated this article into Japanese: http://d.hatena.ne.jp/tdd4ec/20130611/1370904184
I was wondering if you had any Idea how to do something like this in Visual Studio using C++. I am trying to create a replacement .h file where a class is defined. I have to pass an instantiation of this class to the function I need to test. The production code class uses so much other production code, which in turn includes other production code, that my tests ends up including a large chunk of the whole system. This is legacy code and I am not allowed to change the way the original class works. When I create a new include file with the same name as the production code the only thing it complains about is the redefinition of the class. This leads me to believe that is is seeing and compiling both .h files.
Hello James
its not working when the header is in the same folder where it is included, or?
In my case #include “some.h” looks every time first in the same directory and uses fallback the fake. faking works when both are in different folders or when using <> instead of “”.
Is it true for you and you have a solution without faking every file?
My best to you
marco
Hi Marco
When you include using double quotes, the preprocessor will always look in the same directory as the file doing the include.
You either have to move the files or use angle brackets. Here is a good description.
https://programmersought.com/article/95661095609/
When I try to mock ctype.h, it seems to cause a problem with the compilation of CppUTest itself:
In file included from /usr/local/include/c++/11.2.0/bits/localefwd.h:42,
from /usr/local/include/c++/11.2.0/string:43,
from /home/cpputest/include/CppUTest/MemoryLeakDetectorNewMacros.h:48,
from :
/usr/local/include/c++/11.2.0/cctype:64:11: error: ‘isalnum’ has not been declared in ‘::’
64 | using ::isalnum;
| ^~~~~~~
compilation terminated due to -Wfatal-errors.
Hi David
You’ll probably have to include the file before CppUTest is included. Here is a related topic.
https://cpputest.github.io/manual.html#conflicts-with-operator-new-macros-stl
Let me ask, why do you want to fake ctype?
Cheers, James
The target’s compiler includes a non-standard function toint() in the standard library ctype. 🙁