TDD How-to: Get your Legacy C Into a Test Harness
You are getting started with TDD, but have existing code? You want to get some of your challenging C/C++ code under test? You have run into some apparent show stoppers? Don't give up! This article contains a step by step recipe to help get your code into a test harness. It also contains a series of C/C++ code problems that get in the way of unit testing. Each problem named comes with one or more suggested solutions. Also, many of the solutions provide links to articles with more detail.
Many of the problems described arise from trying to get embedded systems code that has only been compiled with the target hardware cross-compiler, to compile off-target. These problems are not unheard of for non-embedded C/C++, so any C/C++ programmer can get some insight into getting your legacy code under test using this approach. Test problems come from dependencies, so If you can relate the C/C++ specific advice to your language, there is something here for non-C programmers as well.
To help you get started, I've created a CppUTest Starter Kit on github that you can use to help get started. The started project describes how to setup your environment and has example tests, code, and mocks.
The approach the solving these problems follows the Crash to Pass algorithm, described in my book and in the linked article. To summarize, Crash to Pass describes a step by step approach to getting problem code under test.
- Create a test file. Build it; force a failure; force success. You are ready to go
- Adjust your error output. Your test build should be set to only report the first error. This saves on scrolling and accidentally chasing a side-effect error.
- Choose the function you want to test. You C++ programmers, choose the class to create.
- Call the code to test from the test case. Don't worry about initialization, yet.
- Make the test file compile. This can take a while an be discouraging in a crusty old code base. Getting the test to compile means adding
#include
s and adjusting the include path in the unit test build. This can be frustrating.- Fix problems one at a time; only fix the first error that is staring you in the face. Try to take satisfaction in changing the error message.
- Don't panic! Look at the Road Blocks below as you discover problems you want some help with.
- You know you are done with 'make it compile' (probably temporarily) once you get a linker error.
- Why start with compiling the test case instead of the production code? It is a smaller first step. The dependencies needed for compiling a call to the production code from the test case is a subset of the dependencies needed to compile the production code. You may discover some show stoppers too.
- Make the code link. The first pass through this you will need to compile the source file that contains the function you are trying to test. You will likely cycle back to 'make the test file' compile. Once its compile time dependencies are satisfied, you are rewarded with more linker errors.
- Stub problem dependencies or compile depended upon production code. When stubbing, do the simplest stub that satisfies the compiler and linker
- Make the dumbest fake like
int my_problem_depedndency(int p) { return -1; }
. - Use my exploding fake generator. Its a bash script you can find in my github CppUTest Starter Kit. It can create an 'exploding fake' for each unresolved external reference.
- You know you are done once the test builds and runs; you will likely see the test crash.
- Make the dumbest fake like
- Track down the crash. Why am I such a pessimist? You should have ignored initialization up to this point. So if you choose code with dependencies a crash is a likely outcome for a C or C++ program. Do the needed initialization, again solving one crash at a time.
- You know you are done once you get the test runner to say OK.
- Finally, make the test more interesting.
- Add some checks to you test. Limit the focus of the test
- Copy, Paste, THINK! As you add the next test, don't just copy, paste, tweak. Make sure to think, extracting common helper functions with descriptive names.
- You know you are done... Left as an exercise to the reader. Congratulations if you got this far.
- Tell me your story
Road Blocks -- Don't Panic
Along the way, you may run into one of more of these specific road-blocks to getting your code under test.
Problem - Non-portable Header File
The legacy code depends on a target specific header file that won’t compile off target.
Solution
Introduce a #include Test Double See #include Test Double. By the way, always prefer to use the real header. Only do this after discovering that using the real header would require changing it. If that file is from a third party, changing it is not really sustainable.
Problem - Header File Leads to Dependency Explosion
The legacy code depends on a header file with many outgoing problem dependencies that the code under test needs very little from.
Solution
Introduce a #include Test Double Again, prefer the real header, fake it if you must. See #include Test Double
Problem - Non-standard Keywords
The legacy code uses non-standard keywords that won’t compile off-target
Solution
Make the non-standard keywords go away with a forced include. See Hiding Non-standard C Keywords for Off-Target Testing.
Problem - asm
The legacy code has asm
instructions that won’t compile off-target
Solution - 1
Make asm
go away using forced include
Solution - 2
Introduce an AsmSpy
to capture the instruction stream and check it in a test case See Spying on Embedded ‘asm’ directives
Problem - OS Dependency
The legacy code interacts with OS concurrency functions.
Solution
Create stub implementation for the OS calls. Keep the stubs very simple at first. Then evolve them to meet the needs of the test case. See three part article series: Unit testing RTOS dependent code – RTOS Test-Double
Problem - #pragma
The legacy code has #pragma
instructions that won’t compile off-target
Solution
Adjust the compiler settings to ignore unknown #pragmas. For gcc use: CFLAGS += -Wno-unknown-pragmas
Problem - restrict
keyword won't compile with gnu g++
The legacy code uses restrict
and won’t compile in a C++ test case
Solution
Adjust the gnu g++ compiler settings to : CXXFLAGS += -Drestrict=__restrict__
Problem - statics are making testing difficult
You have static functions and data, that your tests can't see. If you need to directly access hidden functions and data, your code is telling you it is not modular. But you first have to add tests before you change it.
Solution - 1
Use preprocessor to make static go away. Now there are in the global namespace, but are not advertised in a header file.
Solution - 2
Create a test case and #include the c file in the test, giving full access. See Accessing static Data and Functions in Legacy C — Part 1.
Solution - 3
Create a test adaptor that #includes the c file. See Accessing static Data and Functions in Legacy C — Part 2.
Problem - Platform Dependencies
The legacy code calls library functions that are not available on the test platform.
Solution
Create a set of test stubs for the production code library. Link with that library for development system tests. Make some of the test doubles spies, mocks, etc. JIT as needed.
Problem - Hardware Dependencies
A legacy code file has a few functions that make testing difficult. They may have hardware or OS dependencies.
Solution
Extract the problem functions declarations into a separate header file. Include it from the original file. Extract the problem function implementations into a separate source file. Create a replacement source file made up of test stubs for the problem functions. Link with the replacement for test.
Problem - I need the real C function sometimes and test double other times
If you try to use the linker for test double substitution, you cannot have the original code in the same executable. You need the production code in some tests.
Solution
Create a function pointer with the same signature as the problem function. By default initialize the pointer with the problem function. Change clients to call through the function pointer. Override the function in the tests where needed.
Gotcha
Make sure code runs warning free, if the caller does not see the declaration, C will assume it is a direct function call.
Solution for Linux gcc
Use gcc linker wrapping. See option --wrap symbol
of this Stack Overflow article.
Problems
- Not available be universally available. It was not on Mac OSX, cygwin, or MinGW at the time of this writing.
- Does not work for for public symbols referenced in the same compilation unit.
You've gotten this far, and none of these techniques helped
There is a limit to what can be done with the preprocessor and the linker.
Solution
Make careful changes to the code that enable testing or off-target compilation.
You've gotten this far, take The Legacy C Challenge
- git clone and review the code from https://github.com/jwgrenning/problems-with-extended-c/releases/tag/v0.0
- Make a list of the non-standard C extensions that will cause you trouble.
- Name a solution to each problem
- See a working solution
- https://github.com/jwgrenning/problems-with-extended-c
Tweet
Send me an email about how it went for you. Did you discover any new and interesting 'show-stoppers'? Tweet it, or tell your friends.
Published: April 06, 2014
Latest News
Conference Video - Deep Stack – Tracer Bullets from ADC to Browser
A blank page can be very intimidating, even for a Test-driven developer. Where do we start? Write a test, right? Not always.
more...Podcast on Agile Amped
Here is a short interview with James about TDD and embedded software from the deliver:Agile conference last spring.
more...Programming Research -- Please Participate
Do you have some time to do a simple programming problem in C or C++ for my research?
more...Clean Coders IoT Case Study
My long-time good friend (Uncle) Bob Martin and I have fun programming together firing tracer bullets for distributed water pressure measurement system.
more...Books
James is the author of Test-Driven Development for Embedded C.
Have you read Test-Driven Development for Embedded C? Please write a review at
Amazon
or
Good Reads
.