So you write embedded software in C and you've read about unit testing. You think unit tests will help you write better software, but how do you actually write code that's testable? What are some coding patterns that make unit testing easier?
The first thing to consider is to make sure your software is built from well-defined software units.
Unit testing is all about testing individual parts of your application in isolation. If you design your application from units that have clearly defined boundaries and interfaces, it's going to be a lot easier to test them.
So what's a well-defined software unit?
A software unit is just of group of functions (and maybe some state) that are related together in some way. In embedded software, this is sometimes called a module and implemented in a .c file.
But just having a lot of different .c files doesn't mean that your application has units that can be individually tested. You need your modules to well-defined as well. That is, the interfaces between the modules are clear. When they're not, you get untestable spaghetti. For whatever reason, embedded applications can be especially bad about this.
Here are a few recommendations for creating your own well-defined embedded software units. And since it seems more natural for me, let's call them modules from here on out.
Structure things right
Each software module is implemented in a separate .c file, with a corresponding header (.h) file. The header file defines the module's "public" interface -- the one that other modules use to interact with it. This header file is a "contract" telling you what the module can do and is what other modules
#include to use it. The .c file contains the module's implementation.
Create a black box
You want to draw a clear line between the "public" and "private" portions of your module. Think of your module like a black box. What are you going to hide inside the box (private), and what are you going to make accessible outside the box (public)?
You want to try to hide as much in the box as you can. Only expose just as much as you need to in the public interface.
Functions (or anything else) which are not used by other modules should be inside the box. Put their function prototypes in the .c file and mark them
Keeping a clear line between the public interface and private implementation of your modules makes your code easier to understand and easier to test. Since unit testing occurs at the public interface, the simpler the interface is the simpler your module will be to test.
Clearly define the public interface
The header file defines the public interface and should contain:
- The prototypes of any public functions.
#includestatements for any headers needed by this header file. Typically this needed to get the right types, like when you have a
enums defined by the module and used in the header file.
Keep the implementation separate
The .c file is where the module is implemented. It should contain:
- The implementation of each function.
- Prototypes and implementation of any private functions.
- Any private variables.
- Any private
Special note on globals
Don't expose variables in your module's public interface by
externing them in the header file. They dramatically increase the complexity of your module and make it harder to unit test.
If you do think you need to access to a variable, use getter and/or setter functions to access it. Instead of exposing
int speed in your header use
int get_speed() and/or
int set_speed(). In the spirit of hiding as much in the black box as you can, if you only need to get the value, only include the getter. Be relentless about trying to keep the interface as simple as possible.
Better yet, think about if you really need direct access to the
speed variable. What are the behaviors you want out of the module? Maybe you just need functions like
Limit the headers files you include
Each time you
#include another header in your module, you introduce a new dependency. Dependencies make unit testing your module harder because each dependency will need to be mocked to test the module in isolation.
While mocks are a really useful tool, when a test requires too many mocks it can get unwieldy. All those mocks can lead to brittle tests that can break anytime you change your code.
A few dependencies are okay, but if your module depends on more than a few other modules you may want to thing about changing your design.