Some Tweaks for C Development to Help with TDD

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.

Conversation
  • I get the question about testing static functions whenever I talk to new people about TDD for C. In TDD, a static function is the result of refactoring some longer functions in to well-named steps so that the code tells its story. I don’t want anyone calling the statics, as they may violate the consistency of the module with the statics.

    If you find that you can’t adequately test a static function, it usually means that the code is starting to decay. The module is collecting responsibilities and getting too big. The need to get to the statics for test purposes is a warning sign. Similar to your analysis, the static may be telling you that it should be a visible function on some other (maybe not yet existing) module and tested there.

    In legacy C you’ll find statics that you need to get too. Now you are stuck and have to find a way to test the statics to allow refactoring. You can use preprocessor tricks to change #define STATIC static in production and #define STATIC /*nothing*/ when under test. Of course you have a chance for a name clash if you do this.

    BTW: I recently learned an interesting new trick for testing static functions in legacy code: #include the C file into the test file. All static data and functions are visible. This has a drawback in that a build can have only one test file that includes a given C file. Is this an issue for the Unity generated test runner?

    Stateless functions are great for TDD, but sometimes we need state. I tend to have clear initialization and clean up functions for stateful modules. Even though many embedded systems are only shut down with a power plug or switch, it is still a good policy to have an orderly shutdown so you are sure no resources are leaked.

    Regarding top-down, I tend to think of it as inside-out. I would start with APIs that invoke features and drive them vertically through the layers, faking as we dig deeper. I consider it inside-out because the event or UI stimulus is added after getting core behaviors working.

    • John Van Enk John Van Enk says:

      I have no disagreements. My post is geared mostly at new development and some aspects of it must be approached pragmatically when dealing with legacy code. Thanks for your additions.

      I’m not sure how Unity test runner’s copes with include’ed C files; I’ll have to investigate. Your insight about including the file directly is thought provoking.

  • One more thing: after doing TDD in C I can’t imagine not doing it. C is fussy, free and unforgiving. How did we get anything to work before TDD?

  • Comments are closed.