I've written before about the how to configure Ceedling to run unit tests with an existing project. But after you have your unit test tools set up, it can still be difficult to figure out how to start writing tests. Code that hasn't been designed to be unit tested can be especially difficult. Here a few tips to help you out.
Assert statements are a great tool for programming defensively. This is especially true in embedded systems where we don't typically have a lot of user interface to help our users figure out an error. Often it's better to crash or reset the application programmatically than risk executing the code in and undefined state. But how do you write unit tests for code that can assert?
Including mocks in your tests means that those tests know a lot about the internal implementation of the unit under test. Make a change in the interface of any mocked module, and you not only drive changes in every caller of that interface, you create a cascading series of changes in the tests for each of those callers as well.
Because tests that rely on mocks are prone to breakage, or brittle, they're an active disincentive for making changes to existing code. You're forced to either update the tests with every change you make, allow the tests to break and lose the benefits of unit testing, or just avoid making changes to the code.
For most of my time as an embedded software developer, I almost exclusively wrote code that was going to be run on some microcontroller. I'd fire up the IDE, crank out some code, download (this often took a couple minutes) an run. Then I'd somehow try to figure out if what I had written was correct.
Do your embedded applications ever save any data to flash memory (aka EEPROM)? This where you typically store non-volatile information that needs to preserved if the device is powered down.
This sort of thing is tough to test in the traditional way -- by loading code onto the target and running it -- because it's hard to set and _re-set_ the data in flash for testing. It can also be harder to inspect the data when it's in flash memory.
I don't know about you, but a very natural way for me to think about designing embedded software is from the "outside in". I've been thinking about this a bit recently, and I'm not so sure that's the best approach.
So you write embedded software in C and you think that unit testing might help you do it better. You already know about creating well-defined software modules and how this makes it easier to write unit tests. But what else can you do to make these modules easier to test? What are some more coding patterns that make unit testing easier?
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?
If you want to do embedded test-driven development (TDD), running your automated unit tests on the target is too slow. When you're test-driving, you're running the tests very frequently. You will not want to wait for the tests to download to the target. It will disrupt your flow and you'll get more easily distracted.
Did you know you have options when it comes to creating mocks for your C-language unit tests?
I've been spending a lot of time working with CMock -- since it's used by Ceedling -- but I've just been checking out FFF (the "fake function framework"). It's well done and I think it deserves a closer look.
For embedded test-driven development in C, you need to use a unit test framework to make testing easier. This can take some work to set up. I've created a reproducible environment with Vagrant that you can start using in just a few simple steps.
Maybe you've heard of Test-Driven Development (TDD), and maybe you've even thought it seemed like a reasonable idea. If you haven't tried TDD yet though, you really should. Here's some help to get you started right now.
Every embedded processor vendor now has it's own IDE (e.g. MPLAB, Code Composer Studio, etc.) available to embedded software developers, but you shouldn't use it to compile and build your project. Here's why.
When using a build system for building embedded C applications, we want to be able to automatically track our source file dependencies. This allows us do incremental builds, where each time we build we only build what is necessary based on what has changed. This saves us time...
In this example we'll set up a simple build system for a C application to be complied with gcc. Using a similar method, we could use a cross-compliler (gcc or otherwise) to compile for whatever embedded target we would like.