After all those years, it’s almost disconcerting to see that – at least in the embedded software development domain – there is software written without unit tests. This technical depth needs to be refurbished in the aftermath with a much higher price. I don’t have to fool myself, I did it myself. But why don’t we see the benefits of a little bit of more effort in the beginning, which could save us hours of debugging in the future? I think it’s in the mind of everyone to make results as soon as possible.
With this blog post I would like to point out the major problems and give some inputs on how to make unit testing part of our daily business as software developers.
The lack of knowledge
Unfortunately, they don’t teach you how to write unit tests at university, although in the industries testing is a big part and writing unit test generally takes up much more time than writing software itself.
There is also a fear in embedded programming that you can’t write tests for software which depends on hardware, let alone the programming language C. In the paragraph „Ways to start“, I will give you some entry points, then it’s up to you to deep dive into the world of unit testing and give them sleepless nights an end.
Unit testing is not fun, right?
Have you ever written some software and could validate it a few seconds later with instant feedback? Why do we take the burden to compile it, flash it on the target set up the debugger, make sure everything starts always with the same defaults, run the program up to the point where our new feature shows up, then start it all over again for the next feature with the uncertainity in mind that our previous feature might be breaken? … You see it. It’s not fun to test it on the real hardware either, right.
Ways to start with unit tests
Prepare your software right at the beginning
Setup unit testing for your new project should be a no-brainer just as creating a git repository. Even if you work on a prototype, you need to prepare your setup to make it easy to write unit tests. If the product pursues, how many parts of the software in the final product will come from the prototype? A lot I guess. So write your unit tests with the first line of code!
As per the findings of an internal survey, it was observed that in nearly 50% of the cases (indicated by the blue and orange bars), unit tests were implemented at a later stage, after the codebase had already been significantly developed. Additionally, Test-driven development (TDD) appears to be less commonly practiced in the embedded software development domain. Our developers take the lead here by actively getting involved in our customers development teams to provide support and assistance.
Get yourself a unit test buddy
I remember my first project, there was the demand of the project leader to write unit tests to ensure quality in the work. But my first thought was: How the hell do I write unit tests for an embedded product, how can I execute my program without hardware? Here is the key: Hardware abstraction.
Fortunately I had a senior colleague which gave me an introduction to fake objects like mocks and stubs. After this concept was clear, I was able to move forward by myself.
Break dependencies to hardware and libraries
The most annoying part of unit testing is to get build errors after build errors for unknown symbols. Let’s say if your software depends on the chip vendors library, you might need some defines which are defined in the depths of the library. The easiest way to do this is to write wrappers right ahead so that you just include the wrapper in your application layer instead of the header, which comes directly from the library. You might be afraid that this will blow up your code, but trust me, software with a lot of dependencies is a lot harder to understand than some more files in your repository.
Again, the internal survey shows, that „Hardware Abstraction“, thus Mocking and Faking are the biggest problems.
Some inspiration
In this section I give you some inspiration on how you can add unit tests to already existing code and how to create test doubles, if you have a strong dependency to your chip vendors library. In the following example I set up a unit test for the nRF Connect SDK blinky sample application.
The following picture gives you an overview of the final linkage:
So, set_led_ready()
is the function we would like to test. As we can see, we have a dependency to zephyr’s gpio.h file. In order to test specific behaviour, we need to create a test double for the gpio_is_ready_dt()
and gpio_pin_configure_dt()
functions.
#include "led.h" #include <zephyr/drivers/gpio.h> #define LED0_NODE DT_ALIAS(led0) static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); int set_led_ready(void) { if (!gpio_is_ready_dt(&led)) { return -1; } return gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); } int toggle_led(void) { return gpio_pin_toggle_dt(&led); }
Now the big problem is, that this gpio.h file has a lot of inline functions, makro magic and dependencies itself, which are hard to deal with. Therefore, we create our own gpio.h file which is basically a copy from the original but with as less noise as possible. Yes, you heard me right. Next time you better decouple the dependency to your vendor library. The goal here is just to have a compilable header file which can be linked for our unit test. If you are doing this a lot, you can automate this clean up process with a python script.
From this point, you already have a simple stub. In order to have the full magic of a mock, we call the mock function within our faked gpio implementation. In this sample, Google’s gMock framework was used.
The gpio_mock.cpp file, which contains our gpio.h function implementations (note that gpio.h must be included in gpio_mock’s header file):
#include "gpio_mock.hpp" #include <assert.h> std::unique_ptr<GpioMock> pGpioMock; void GpioMock_createNice(void) { pGpioMock = std::unique_ptr<GpioMock>(new ::testing::NiceMock<GpioMock>()); } bool gpio_is_ready_dt(const struct gpio_dt_spec *spec) { assert(pGpioMock); return pGpioMock->gpio_is_ready_dt(spec); // call mock } int gpio_pin_configure_dt(const struct gpio_dt_spec *spec, gpio_flags_t extra_flags) { assert(pGpioMock); return pGpioMock->gpio_pin_configure_dt(spec, extra_flags); // call mock } int gpio_pin_toggle_dt(const struct gpio_dt_spec *spec) { assert(pGpioMock); return pGpioMock->gpio_pin_toggle_dt(spec); // call mock }
The gpio_mock.hpp file:
#ifndef GPIO_MOCK_HPP #define GPIO_MOCK_HPP #include <gmock/gmock.h> #include <memory> extern "C" { #include "gpio.h" } class GpioMock { public: MOCK_METHOD1(gpio_is_ready_dt, bool (const struct gpio_dt_spec *spec)); MOCK_METHOD2(gpio_pin_configure_dt, int(const struct gpio_dt_spec *spec, gpio_flags_t extra_flags)); MOCK_METHOD1(gpio_pin_toggle_dt, int(const struct gpio_dt_spec *spec)); }; extern std::unique_ptr<GpioMock> pGpioMock; void GpioMock_createNice(void); #endif
Creating such mocks out of an existing header file can also be easily automated with python.
Finally, our test looks like this:
#include <gtest/gtest.h> #include <gmock/gmock.h> extern "C" { #include "led.h" } #include "zephyr/drivers/gpio_mock.hpp" using namespace ::testing; TEST(LedTest, set_led_ready_success) { GpioMock_createNice(); EXPECT_CALL(*pGpioMock, gpio_is_ready_dt(_)).WillOnce(Return(true)); EXPECT_CALL(*pGpioMock, gpio_pin_configure_dt(_,_)).WillOnce(Return(0)); EXPECT_EQ(set_led_ready(), 0); pGpioMock.reset(); }
Now to recap: We have substituted the Zephyr include directory with our test double include directory. In our unit test project, the gpio.h functions are linked to their implementation in gpio_mock.cpp. This is represented in the CMakeLists.txt file of our unit test project:
... add_executable( led_test led_test.cpp ${CMAKE_CURRENT_LIST_DIR}/../src/led.c ${CMAKE_CURRENT_LIST_DIR}/test_doubles/zephyr/drivers/gpio_mock.cpp ) target_link_libraries( led_test GTest::gtest_main GTest::gmock_main ) target_include_directories(led_test PUBLIC ${CMAKE_CURRENT_LIST_DIR}/../src ${CMAKE_CURRENT_LIST_DIR}/test_doubles) ...
To get a better overview, you can checkout the complete repository on GitHub. Keep in mind that this is just for inspiration. In general I would recommend to further break the dependency to the SDK.
How to force yourself writing unit tests
To round up this blog post, here are a few suggestions for you, that will make writing unit tests easier:
- Make it a habit. If you work on a bug, create a unit test for it
- Configure your CI pipeline (Another thing to setup right at the beginning of the project) to only allow merge requests with code coverage higher than 80%
- Make it visible by using code coverage: If new software is written without unit tests, code coverage will decrease. This should be an alarm signal for everybody
- Make it visible by presenting the number of tests, to see the progress of your work
- Think of unit tests as a type of documentation
Further Reading
If you still struggling with this topic, I highly recommend to check out the following books and links:
- Test-Driven Development for Embedded C – James W. Grenning
- Wrestle Legacy C into Tests
- Working Effectively with Legacy Code – Michael Feathers
- Clean Code and Clean Coder by Uncle Bob
- Using GoogleTest and GoogleMock for embedded C
- Ceedling unit testing framework
What are your experiences and which obstacles did you came across? Let me know in the comments!