I find that test driven development can be a good tool to employ when writing C programs. Unfortunately, some of the habits I built up from before I learned how to do test driven development in C took a long time to die. Here are a few things I’ve learned not to resist.
Static functions are hard to test; don’t feel bad for not using them.
Static functions are hard to mock properly and can’t be independently tested outside of the compilation object. Sure you can use some preprocessor magic to un-static a function in a test environment, but this should probably not be the norm. I have two reasons for this: first, static functions imply that some other function in the compilation object may not have properly isolated tests. The second reason is that you want to test code as similar as possible to the code that’s actually going to run on the target. This becomes more important as the criticality of the software rises (but is good practice all the time).
If you find yourself using a static function, ask yourself if it’s really a function that must only ever be used here, or if it’s functionality that should be tested independently in another module. The static keyword is still an important tool when it helps with necessary performance or a set of order-dependent operations.
Stateless and reentrant functions are your friends; use them when possible.
This one is simple. It’s easier to verify the correct operation of a function when the operation of the function is entirely determined by its inputs. If it has dependencies on other external program state, then these preconditions must be detected, controlled, and accounted for in the test.
Start from the top, and build your way down.
When doing new development, I find that starting from the top and building down works best. In other words, start by implementing your main function or your API’s initialization function. In your test, mock out the lower level calls that it will make. Continue this until your program reaches the edges. This will probably encourage your design to take on some layers. The top layer is your main file, the bottom layer(s) are your edges (the code that doesn’t call anything else), and the stuff in the middle is business logic. Normally, these layers will only ever call into the layer below because writing isolation tests with mocks for a function that calls another in the same layer is pretty hard. Instead of trying to hack your way around it, move the callee into a lower layer.
Don’t use globals unless you have to. If you must, don’t touch them directly; use an accessor.
This is related to my second point. It’s also probably not the first time you’ve dealt with this advice. I do have a specific distinction I want to bring up: accessors can be mocked out properly while global variables must be extern’ed into the test and properly initialized. I find that tests using mocked out global accessors are much easier to follow than those that have to setup global state ahead of time. (This should go without saying, but function-like macros don’t count as accessors.)
Test driven development may not feel like a natural fit for C at first, but a few tweaks to your methodology can help you get back into the rhythm you’re used to when test driving in other languages.