What can make your code easier to test is if you conceptually break it down into manageable layers which makes it possible to replace normally called functions that are out of the scope of the test? This is normally done with test stubs, fakes, or mocks (conceptually similar test devices). These test devices can be designed or configured to record how they get called and can be set to return predetermined values to the callers, which makes it possible to accurately measure the outcome of your tests.
Michael Feather coined the test “seam” concept in his book Working Effectively with Legacy Code. In C, there are not many options for seams. Two of the more practical choices in C programming are:
- “Link Seams”, where one tends to compile a subsection of the entire code base you are testing, and replaces modules with similarly named modules containing test stubs, mocks or fakes.
- The advantage of this is that it forces you to keep your modules small and well designed so that they are testable,
- The disadvantages of this are that until you have tools designed to support them all, the number of test executables that result from getting each code module in a large code base under test is not easy to manage and is not scalable. For example, with Eclipse CDT, one has few options but to have one test project per module being tested. Sometimes one can combine multiple modules under one test executable, but this can make it difficult to catalogue where a specific module is being tested.
- “Function Pointer Seams”, where one replaces direct calls to functions with calls using function pointers. This enables one to break dependencies by replacing functions out of the scope of your tests with your own stubs, fakes or mocks.
- The advantage of this is that it allows you to keep the test code all in one place. It also gives you a lot of flexibility to replace any function as required. It is also very portable between different systems.
- Disadvantages are that each function pointer uses RAM (something firmware programmers cannot always afford to use up), the code runs slightly slower, and the source code becomes cluttered with the extra code defining function pointers (making it less readable). Function pointers also make it difficult to build call trees of your code when using static analysis tools.
- Generally, the much larger test projects that are possible when using this method take a lot longer to compile. For a large code base, you may still want to break your code up into two or smaller logical segments. This can be a consideration for other languages apart from C.
Removing the disadvantagesEmbed from Getty Images
I will explain a simple pattern that removes most of the disadvantages of the function pointer methodology.
Normally a function pointer is defined as follows:
and one calls this code as follows:
If other modules need to access this function, one needs to expose the function pointer as a global in the module’s header file as follows:
As suggested earlier, in production code you usually don’t want to use function pointers. To get the best of both worlds, (where functions are replaceable in test code but don’t have the associated overheads and transparency issues in production code) you need to resort to the use of macros. For test code, you want to expose the function pointers, but in production code, you want to expose your functions as direct calls.
The way this is done is as follows. In a commonly exposed header file (present in most projects, let’s assume its called defines.h), you need to define the following macros with a conditional compile:
When declaring functions that need to be replaceable you need to define them as follows:
And your prototype needs to be declared as follows:
Notice how similar the code looks to the original, and once you get used to it, it is still very readability. When you compile for testing, you simply need to build with “TESTING” defined.
Assuming that in your test code you want to replace
You can do the replacement in the following way:
… and anywhere that the code being tested calls function_to_be_replaced, it will land up calling test_stub instead.
ConclusionEmbed from Getty Images
You should obviously, as a matter of principle restore the functions that have been replaced to their original state after each test. This is to avoid your code being affected in any unanticipated ways by other tests.
Although there is a small readability penalty, I believe this is a small compromise when you consider the very much improved flexibility, manageability and scalability that you will gain in your testability.
If you have further ideas or considerations to offer when it comes to making large C code bases more testable without the associated compromises, I would be very interested to hear your thoughts.