Recently, I’ve been working on a project where I’ve been given some flexibility to introduce unit testing to a C code base along with some refactoring and modernization necessary to make that happen. The code base has been going strong and performing well for years but, even so, there is lots of room to help make the code base easier to understand, easier to maintain, and easier to scale. While I’ve spoken about this before on my personal blog it has been five years since I’ve done so and it appears to be time for a new take on this subject. In this post I will cover the tools that I chose this time around and why. My plan is to write another post that covers convincing a team to test and how to make it easy for them to start writing those tests.
Unit Testing Framework
This time around I’ve chosen to use googletest. There are several major reasons for this:
- It is used for several major projects and is currently being supported.
- It provides a relatively simple interface for setting up tests and asserting that the outcome is what you expected.
- The tests themselves will be written in C++
Aside on C++
My goal while introducing unit testing is not just to write unit tests but to improve the quality of the code as much as I possibly can. While the current code base is written in C, C++ provides some opportunities to make both immediate and long term improvements. Some of the short term improvements include:
- Using standard library containers including strings and vectors to make the code simpler and easier to read.
- With newer types such as maps and vectors it would likely be possible to simplify logic.
- Updating current pointers to smart pointers to make sure that the data is valid where it needs to be and is cleaned up appropriately when we’re done.
Long term improvements include:
- Refactoring the large amount of global data that is currently present into objects.
- Pulling out portions of logic from long functions into the classes created from the global data. This will provide clean interfaces to act on the data. These classes would likely be easier to write tests for than the functions they would replace.
Now back to the topic at hand: testing
If you’re trying to test a unit of functionality (a function in the case of C/C++) it is best to do in isolation for a couple of reasons:
- You need ensure that the unit is doing what you intended.
- You want to control the incoming data and any side effects that occur in other functions. This way you know what the output is going to be and can assert that it matches what you expected.
- You want the tests to be fast and not dependent on anything that will be slow to respond (e.g. a database or the network)
This means that having the capability to mock or fake functions is critical to writing good tests for code.
Google Mock seemed to be the obvious choice for mocking/faking because it is included with Google Test. It, however, didn’t work for me because it presumes that the code being faked is written in C++ and is split into classes. To be fair I didn’t read every nook and cranny of the user guide. The introduction and simple examples given, however, presume the code under test is a class from which the mock class can be derived making C++ the first class citizen/target of the library. While I am introducing C++ to the code base and, at a later date, may come back to Google Mock to handle this code I decided to look for something else that to fit my needs more closely.
After some searching I found Fake Function Framework (aka FFF). FFF is a header only based option that uses macros to generate fakes for functions. The fake creation macros come in various flavors that allow for almost any number of arguments (including variadic functions like printf) and return values. Additionally, they track statistics such as call count and implement argument capturing to make sure that the unit under test called it correctly. Return values can also be set for the fake to return so that the unit under test doesn’t fail because it didn’t get back what it expected. Overall, this is a very nice choice given the code that I’m working with and is still under active development.
Note that the fakes generated by any framework cannot cover all cases. There will always be fakes where you need custom logic to handle complex data and the code I’m working in is no different. In many cases the functions that you may want to write custom logic to fake, such as those that return data from a database, can be grouped together into their own C / C++ file and excluded from the linking step of the test executable. This is an example of using seams (in this case, the linker seam) to separate concerns and make the code testable.
In this post I’ve shown the frameworks I chose for implementing unit tests and fakes for a C code base and laid out why I chose them for this particular project.
I recognize that this post isn’t very detailed on why you’d want to unit test. That, along with how to convince developers and project managers to invest the time in testing and make it easy for them to do so, will come in my next post.