Bare-metal? Unit-tests? Why not!

Ok, so I’ll try to keep this brief. 

Some context

Unit testing is a good thing, and you should be doing it (don’t worry: that was just me talking to myself, since I miserably fail to follow my own advice). “Well, why?”, you might ask. Simply put, it’s a nice way of ensuring that your software modules/classes/thingies work in the way they should in isolation. Even though coming up with tests takes time, overall this practice tends reduces development complexity and headaches, forces you to write marginally better APIs and serves as a makeshift documentation for your code. 

Stolen from Reddit.

Good. When we’re talking about unit testing for desktop targets, framework options are plentiful. For the C/C++ world, three options pop in mind fairly quickly, but the list is gigantic. Same goes for most of the popular languages that kids use these days. 

However, in the embedded scene, the scenario is different. Computing power tends to be much more limited, and there are often interactions with external hardware that make fully isolated tests hard or unfeasible. If you’re running a *nix box, you can perhaps still profit from the one of the aforementioned frameworks (or try some tailor-made tool), but if you’re dealing with bare-metal targets, you’re pretty much out of luck. On her book, “Making Embedded Systems”, Elicia White skims over some testing practices for embedded systems, but the gist is that there’s no one-size-fits all approach. This thread pretty much echoes this feeling. 

My 2 cents

Getting to the point. As mentioned in another post, I’ve been working with a colleague on a firmware for a VFD. The firmware quickly racked quite a few software components, and keeping track of what worked and what didn’t was getting way to confusing. 

So, I drew some (*ahem*, a lot of) inspiration from Martin Dørum’s unit-test framework Snow – check it out! -, and started working on a bare-bones version of it for the STM32F10x I was using. The result is L_TEST, my attempt at an minimalist unit-test framework for bare-metal targets. The idea behind L_TEST is allowing the user to quickly define test routines and use basic assertion macros to ensure everything is working as it should. Test cases should look straightforward and clear. I really enjoy Dørum’s take on this, so L_TEST employs pretty much the same syntax as Snow does. Take a look at a simple example:

L_TEST uses printf, so you’ll need to implement the _write syscall. The output is color formatted, and should play nice with bash & friends:

Contrary to Dørum’s Snow, L_TEST is unfortunately not header-only, but the implementation is fairly light: 260 bytes for the aforementioned STM32F10x compiled with the -g flag, as shown in the map file below:

The addition of such functions also allowed the macros to be generally simpler, which in turn also reduces code footprint. Still a win, even though you have another source file to add to your list. Speaking of macros, the list still isn’t very plentiful, but here are a few:

  • L_TEST_MODULE(mname, mdesc): Define a test module, with a name and a string description. Test cases should be defined within its body
  • L_TEST_CASE(desc): Define a test case with a description. Test cases can only be defined withen a L_TEST_MODULE body.
  • L_TEST_ASSERT(val): Asserts that the value is non-zero. If the assertion fails, an error message is printed and the test case is aborted.
  • L_TEST_ASSERTEQ_INT(val, ref): Asserts that val == ref, assuming integer values. If the assertion fails, the expected and current values will be printed, and the test case will be aborted.
  • L_TEST_ASSERTEQ_FLT(val, ref): Asserts that the RPD between val and ref is less than 0.1%. If the assertion fails, the expected and current values will be printed, and the test case will be aborted.
  • L_TEST_ASSERTEQ_STR(val, ref): Executes a strcmp between the two supplied strings. If the assertion fails, the expected string and the supplied string are printed.

You can take a look at the full list here.

And thus, 

Unit-testing functionalities like this are just the tip of the iceberg. There’s a lot more that can be done using dummies, mocks, stubs and so on. L_TEST is not, by any stretch of the imagination, an attempt at a full-fledged test framework. However, it did suit my project’s simple needs, so it might suit someone else’s.  You can take a look at L_TEST on GitHub; have your go with it, and let me if you find (or better, when you find) any bugs/issues. Suggestions are welcome! 

’til next time.