Embedded software vs unit testing, mocking and dependency injection
January 20, 2024

As an embedded systems developer, I have often faced the challenge of integrating effective unit tests in an environment where hardware dependencies are an integral part of the development process. In this article, I want to share my experiences and techniques that have helped me overcome these challenges. I will focus on the use of mocks and dependency injection, demonstrating how these methods can significantly ease the unit testing process in embedded systems. This article is a collection of my thoughts and practical advice that I hope will be helpful to other developers in this rapidly evolving field.

When I started my career as an embedded systems programmer, I often heard that unit testing was fantastic, but not in the world of embedded systems. It was difficult for me to accept this statement. I was determined to challenge the status quo. I wondered. why can't we, in the embedded systems world, adopt all the best practices used around the world?
So I took the first best unit testing framework and wrote my inaugural test case. What an exciting adventure! I launched it and...

$ ./run_tests Running unit tests... Segmentation fault (core dumped) Backtrace: #0 0x00007f9c88931780 in i2c_read_byte () from /path/to/libi2c.c #1 0x00007f9c887d5a15 in sensor_read_data () from /path/to/libsensor.c #2 0x00007f9c887d63f2 in main () from /path/to/test_executable Aborted (core dumped)

It turned out that I had not thought through the hardware dependencies that existed in the project. How to overcome this? And that's what this article is about.

What is a mock?

A mock is a simulated object that mimics the behavior of a real object in a controlled way. It is used in software testing to replace dependencies and simulate their behavior during unit testing. Mocks are often used when real dependencies are difficult to set up or interact with during testing.
They allow developers to isolate the code under test and focus on specific scenarios without relying on the actual implementation of dependencies.
Mockups can be created manually or using mocking frameworks such as GMock.

Dependency injection

We talk about dependency injection when we want to indicate that a module can easily replace a part of the code it uses.
But why and when would we want to do this?
One example is when we need to change the behavior of a class that is used in a different hardware version than before. Another reason might be to use a mount instead of a production module for unit testing.

Let's look at the following simple example:

Since the "Testable code" module uses the "Production module", this enables easy injection, resulting in cleaner and more maintainable code. By creating a separate executable file with various dependencies injected into "Testable code", we can effectively use it for testing purposes.
This approach improves our example, as shown below:

Runtime dependency injection

But let's consider how we can implement dependency injection in a relaxed way, tailored to everyday business needs.
I will introduce you to two methods.
The first method is to use dependency injection at runtime. This can be achieved by passing a polymorphism-based object by reference when creating our "Testable code." Below you will find an example that can be used in production code:

// foo.h #pragma once class Dependency { public: Dependency() {} ~Dependency() {} virtual void DoSomething() {} virtual int GetSomething() {} }; class Foo { public: Foo(Dependency* const dependency) : dependency_(dependency) {} ~Foo() {} void bar() { dependency_->DoSomething(); } int get() { return dependency_->GetSomething(); } private: Dependency * const dependency_; };

Compile-time dependency injection

When injecting dependencies at compile time, the task of the consolidator (linker) is to link the appropriate implementation.
It is important to know the build system you are using, but it is worth trying this approach. This way you don't have to modify your API to enable module injection.
This can be achieved during compilation (or more precisely, during consolidation).
Here is a very simple example, which I will expand on in the next section:

// foo.h #pragma once class Dependency { public: Dependency(); ~Dependency(); void DoSomething(); int GetSomething(); }; class Foo { public: Foo() {} ~Foo() {} void bar() { dependency_.DoSomething(); } int get() { return dependency_.GetSomething(); } private: Dependency dependency_ {}; }; // dependency.cpp // Implementation in separate file! #include "foo.h" Dependency::Dependency() {} Dependency::~Dependency() {} void Dependency::DoSomething() {} int Dependency::GetSomething() {}

By separating the function declarations from the definitions in the Dependency class, we have the ability to compile a separate file with an alternative definition of these functions. In the next section, I will explain how this process works.

Mocking with compile-time dependency injection

Now, using GMock, I'll show you how to inject mocs instead of our "production" dependencies.
Please take a look:

// dependency_mock.h #pragma once #include "gmock/gmock.h" class DependencyMock { public: MOCK_METHOD(void, DoSomething, (), ()); MOCK_METHOD(int, GetSomething, (), ()); MOCK_METHOD(void, _constructor, (), ()); MOCK_METHOD(void, _destructor, (), ()); // singleton static DependencyMock & get_instance() { static DependencyMock instance; return instance; } private: DependencyMock() = default; }; // dependency_mock.cpp #include "dependency_mock.h" #include "dependency.h" // This is the place where mocks are injected Dependency::Dependency() { DependencyMock::get_instance()._constructor(); } Dependency::~Dependency() { DependencyMock::get_instance()._destructor(); } void Dependency::DoSomething() { DependencyMock::get_instance().DoSomething(); } int Dependency::GetSomething() { return DependencyMock::get_instance().GetSomething(); } // test.cpp #include "foo.h" #include "dependency_mock.h" #include #include using namespace ::testing; class Fixture : public Test { protected: // To verify singleton mock object. Usually it happens in destructor void TearDown() override { ASSERT_TRUE(Mock::VerifyAndClearExpectations(&DependencyMock::get_instance())); } }; TEST_F(Fixture, WhenCreatingFooThenDependencyIsCreated) { EXPECT_CALL(DependencyMock::get_instance(), _constructor).Times(1); auto foo = std::make_shared(); EXPECT_TRUE(foo != nullptr); }

Let's take a closer look at the provided code. We implemented the singleton pattern in the DependencyMock class to avoid explicitly instantiating the mock class. Instead, we set our expectations about which functions should be called and with what parameters. This approach requires us to explicitly clear and verify the mock expectation.
In a typical scenario, this would be done in the destructor of the mock, but since we are using the singleton pattern, we need to handle this in the final (tear down) phase of our test suite, like this:

void TearDown() override { ASSERT_TRUE(Mock::VerifyAndClearExpectations(&DependencyMock::get_instance())); }

How can we set expectations for the constructor? In the "WhenCreatingFooThenDependencyIsCreated" test case, it is clear that we also expect the "Dependency" object to be created.
Let's look at the whole process to get a more comprehensive understanding of how it works.
It's worth noting that using the "MOCK_METHOD" macro, we generate a member function on which we can set our expectations, such as:

EXPECT_CALL(DependencyMock::get_instance(), _constructor).Times(1);

Well... but how can we be sure that our Dependency class has been created? We just discussed the DependencyMock class. This is where the magic happens. With dependency injection at compile time, we have the ability to define our Dependency class in any way we want.
And for mocking, we can also redirect the call to our mock object, as shown in the dependency_mock.cpp file:

Dependency::Dependency() { DependencyMock::get_instance()._constructor(); }

With this line, every time we create a Dependency object during unit testing, we actually call DependencyMock.
This is the reason why everything can work without negatively affecting the API.

Summary

Unit testing in the world of embedded systems can be a challenge due to hardware dependencies. However, there are techniques, such as using mocks and dependency injection, that can help overcome these challenges. Mockups allow developers to simulate real-world objects during testing, while dependency injection allows for easy replacement of portions of code. Frameworks such as GMock can be used for mocking with dependency injection at compile time.

References

GoogleTest User's Guide

#GoodByte #EmbeddedSoftware #DependencyInjectionInEmbedded