We run an Escape Room which is, at some places, using Arduinos or bare ATtiny microcontrollers where they control some actuators (like motors), but also effects e.g. on WS2812b LED strips. We need the code to be reliable, and development to be fast – i.e. logic bugs should preferredly be found before downloading the code to the Arduino. So, we unit test the code. It took a few hours to get it working nicely and without polluting the VCS. If you also want to unit test your code, here you go.
Unit testing means testing very small code units, normally functions. For example, a function calculates a brightness value based on a timestamp, and the brightness is pulsating at a frequency of 1 Hz. A unit test would verify if it calculates the correct brightness value for the timestamps 0 ms, 100 ms, 600 ms, 1000 ms, 2000 ms (should be the same as 1000 ms and 0 ms). If those values are correct, the probability is high that the function is working correctly.
Unit Testing Arduino Code?
Now how are the IOs and all other Arduino specific functions like sleep() etc. going to be simulated?
The answer is: not. Even though there are libraries which simulate an Arduino to a certain extent, it is easier to limit the scope of the unit tests and exclude the hardware. To state more clearly: Code can be separated into two categories.
- Hardware Code which interfaces with IOs and interrupts and so on
Domain Logic which describes what your program does
In the previous example, the brightness function is domain logic which calculates the nice pulsating brightness values. The hardware code is then using this brightness function to send the brightness values to the LED aka. hardware.
This differenciation is crucial for unit testing and also enforces writing cleaner code.
Unit Testing with Catch2
Many C++ unit testing frameworks exist. I have chosen Catch2 (reasons are out of scope). The project can easily be copy/pasted for new Arduino projects.
The workflow to get Unit Testing support is as follows:
- Domain logic code goes to separate files, ideally with header files. For example, demo.cpp and demo.h.
- The unit tests go to the spec/ directory. Check the Catch2 documentation and the demo.spec.cpp file.
- For clarity, there is one spec file (unit test file) per domain logic file. Each spec file includes the corresponding domain logic file.
Domain logic code files that live outside of an Arduino project directory are linked into the project directory (ln -s) as otherwise the compiler will not find them. Keeping domain logic code outside of project directories (and just placing a link there) allows to easier re-use them in different projects. For example, you might have a library for communication between two Arduinos, a sender and a receiver. Both use the same protocol (same .cpp files), but they are two different projects. Using symbolic links avoids code duplication.
If you want to get started with unit testing and want to give Catch2 a try, here you go! It requires some knowledge about unit testing and using C++ header files, but it definitely improves code quality.