Click here to Skip to main content
14,972,394 members
Articles / DevOps / Unit Testing
Article
Posted 6 Nov 2015

Stats

57.2K views
1.6K downloads
14 bookmarked

Using GoogleTest and GoogleMock frameworks for embedded C

Rate me:
Please Sign up or sign in to vote.
4.80/5 (6 votes)
8 Nov 2015CPOL12 min read
Presenting techniques for effective usage of Google unit test frameworks in embedded environment.

Introduction

This article is about unit testing. Although the unit testing is quite popular in various programming languages, the embedded C programming is usually overlooked. This is ironic because embedded programmer may benefit from the unit testing like no other. If you work with embedded systems then you must know how hard it is to debug your code. You add logs, you connect JTAG emulator, you sweat, you curse... if so, this article was written to help you. Give it a try.

In this article I use GoogletTest and GoogleMock frameworks for unit tests writing. I assume that the reader is familiar with these frameworks. Otherwise you should read GoogleTest and GoogleMock documentation first. While the GoogleTest could be easily adjusted to C testing, the GoogleMock has a little to propose to the C programmer. The GoogleMock framework was designed for mocking C++ interfaces and it relies on the virtual functions mechanics, which is lacking in the C language. Without mocking interfaces, the unit testing becomes very limited. In this article I suggest two different approaches for C functions mocking and then bundle them into one solution that addresses majority of the unit testing scenarios. This article also covers some design aspects to help you in unit tests writing.

The techniques that I describe in this article are not new. Some of them were borrowed from the "Test driven development for embedded C" book. The book is an excellent C unit testing resource but, in my opinion, it should have described in more details how to utilize frameworks for the C functions mocking. There are a lot of articles and frameworks over the Internet trying to address the C mocking problem, but I found them incomplete. Most of them are limited to GNU/Linux environment and provide no practical guidance for unit testing writing. Also, in this article, I propose HOOK mocking technique that I haven't found in any other framework.

Testing C functions with GoogleTest

Let's say we have a simple queue code and we want to put it under test. The code resides in Queue.c file.

C++
#define QUEUE_SIZE 15

static int queue[QUEUE_SIZE];
static int head, tail, count;

void QueueReset()
{
    head = tail = count = 0;
}

int QueueCount()
{
    return count;
}

int QueuePush(int i)
{
    if (count == QUEUE_SIZE)
        return -1;
    queue[tail] = i;
    tail = (tail + 1) % QUEUE_SIZE;
    count++;
    return 0;
}
...

The trick is simple. After creating C++ file for the test, include the C file into it and the C code will become accessible to the test code. Below is the code of QueueUnitTest.cpp file.

C++
#include "Queue.c"

TEST_F(QueueUnitTest, ResetTest)
{
    EXPECT_EQ(0, QueuePush(1));
    EXPECT_EQ(1, QueueCount());
    QueueReset();
    EXPECT_EQ(0, QueueCount());
}

TEST_F(QueueUnitTest, CountTest)
{
    for (auto i = 0; i < QUEUE_SIZE; i++)
        EXPECT_EQ(0, QueuePush(i));
    EXPECT_EQ(QUEUE_SIZE, QueueCount());
}
...

Why not to include Queue.h instead of Queue.c? Indeed, this will allow you to call the C functions declared in the h file, but you won't get access to the static functions and the static global variables declared in the C file.

Now, let's discuss the global variables. There are no classes in C, but in well designed C program, the code is divided into modules which reside in separated files and the global variables are usually declared to be static. Thus, C module could be considered as C++ class equivalent and module's global variables are similar to the class private static fields.  If so, we should be able to initialize them before running a test. Consider adding ModuleNameInit method and put the global variables initialization into it. Here is an example of such initialization in Queue.c file.

C++
void QueueInit()
{
    head = tail = count = 0;
}

void QueueReset()
{
    QueueInit();
}
...

The test fixture class can now call the QueueInit function and prepare the module for its test. The GoogleTest framework creates a new fixture object for every test and, thus, the QueueInit function will run before each test execution.

C++
#include "Fixture.h"
#include "Queue.c"

class QueueUnitTest : public TestFixture
{
public:
    QueueUnitTest()
    {
        QueueInit();
    }
};

TEST_F(QueueUnitTest, ResetTest)
{
    EXPECT_EQ(0, QueuePush(1));
    EXPECT_EQ(1, QueueCount());
    QueueReset();
    EXPECT_EQ(0, QueueCount());
}
...

Mocks in C code

In previous section we saw how to test the C functions, but what if the function we test calls another function, that resides outside of our module? It could be a log service that our queue should call on push operation or, may be, it is required to signal GPIO if the queue is full. To handle such situations we need to create mock objects and teach the code to call mocks instead of the production code. I found two useful techniques for the C functions mocking and I suggest choosing between them based on the nature of the mocked functions.

Mocking services

The first technique suggests to cut functions off your code and replace them with fake implementations. These fake implementations, in turn, will call to the to the corresponding mock objects. This approach is mostly suitable for hardware and OS services, such as timers, GPIO-s, OS logs, IPC entities and so on. Indeed, these types of services are not related to your application code and should not be put under test. Their fake implementations provide your application with kind of virtual runtime environment.

Let's say we have an oscillator and GPIO registers and we wrap them with the following functions.

C++
int GetTime()
{
    // Code that reads oscillator register
    ...
}

int ReadGpio(int line, int *value)
{
    // Code that reads from GPIO line
    ...
}

int WriteGpio(int line, int value)
{
    // Code that writes to GPIO line
    ...
}

These functions should be re-implemented in our testing code. These implementations will call to the corresponding mock objects.

C++
int GetTime()
{
    return GetMock<OscillatorMock>().GetTime();
}

int WriteGpio(int line, int value)
{
    return GetMock<GpioMock>().WriteGpio(line, value);
}

int ReadGpio(int line, int *value)
{
    return GetMock<GpioMock>().ReadGpio(line, value);
}

In the code above, the mock objects are retrieved from a singleton that stores all the mock objects. You will find a detailed explanation of how it works in the next sections.

Below is an example of code that calls to the services. The code resides in the ShortCircuit.c file.

C++
void ShortCircuit(int timeout)
{
    int startTime = GetTime();
    while(GetTime() - startTime < timeout)
    {
        int signal;
        ReadGpio(0, &signal);
        WriteGpio(1, signal);
    }
}

The unit test for the ShortCircuit function is shown below.

C++
#include "Fixture.h"
#include "ShortCircuit.c"

class ShortCircuitUnitTest : public TestFixture
{
public:
    ...
};

TEST_F(ShortCircuitUnitTest, ShortcutTest)
{
    EXPECT_CALL(GetMock<OscillatorMock>(), GetTime()).Times(3)
                .WillOnce(Return(10)).WillOnce(Return(100)).WillOnce(Return(111));
    EXPECT_CALL(GetMock<GpioMock>(), ReadGpio(0, _)).Times(1)
                .WillOnce(DoAll(SetArgPointee<1>(1), Return(0)));
    EXPECT_CALL(GetMock<GpioMock>(), WriteGpio(1, 1)).Times(1);

    int timeout = 100;
    ShortCircuit(timeout);
}

Mocking C modules

The second mocking technique is more suitable for mocking your own code. Suppose there is a C module that uses previously defined queue code. Let's say we have code that is triggered by a packet received from the network and the packet content is pushed into the queue (see Isr.c file).

C++
int OnDataReceived(int data)
{
    if (QueuePush(data) == -1)
    {
        return -1;
    }
    return 0;
}

Our goal is to put expectations on the queue module functions. For this purpose, we add, at the beginning of the functions, calls to the corresponding mock objects. To do so, we will use macro magic. I know, that as a C programmer you will appreciate the macro magic, but first we need to write queue mock class. To allow easy access and manipulation of mock objects, our mock classes must inherit from the ModuleMock class. The ModuleMock class is described later in this article.

C++
#include "Queue.h"
#include "ModuleMock.h"

class QueueMock : public ModuleMock
{
public:
    MOCK_METHOD0(QueueReset, void());
    MOCK_METHOD0(QueueCount, int());
    MOCK_METHOD1(QueuePush, int(int));
    MOCK_METHOD1(QueuePop, int(int*));
};

The macro that we are going to put into the queue code calls to the corresponding mock in the unit test run and does nothing in the production code. I call these macros HOOKs.

C++
#if defined(__cplusplus)

#define MOCK_HOOK_P0(f) {return GetMock<MOCK_CLASS>().f();}
#define MOCK_HOOK_P1(f, p1) {return GetMock<MOCK_CLASS>().f(p1);}
#define MOCK_HOOK_P2(f, p1, p2) {return GetMock<MOCK_CLASS>().f(p1, p2);}
...
#else

#define MOCK_HOOK_P0(f)
#define MOCK_HOOK_P1(f, p1)
#define MOCK_HOOK_P2(f, p1, p2)
...
#endif

The GetMock<MOCK_CLASS>() returns a reference to a mock object of type MOCK_CLASS. The MOCK_CLASS is a placeholder for the real mock class, which need to be defined with the #define directive in the QueueUnitTest.cpp code. We will discuss how exactly it works in the next section.

Here is the code of Queue.c with the HOOK macros in it.

C++
#include "MockHooks.h"
...

void QueueReset()
{
    MOCK_HOOK_P0(QueueReset);
    QueueInit();
}

int QueueCount()
{
    MOCK_HOOK_P0(QueueCount);
    return count;
}

int QueuePush(int i)
{
    MOCK_HOOK_P1(QueuePush, i);    
    ...
    return 0;
}

int QueuePop(int *i)
{
    MOCK_HOOK_P1(QueuePop, i);
    ...
    return 0;
}

Finally, we can write expectations in our tests (see IsrUnitTest.cpp file).

C++
#include "Fixture.h"
#include "Isr.c"

class IsrUnitTest : public TestFixture
{
public:
    IsrUnitTest()
    {
        IsrInit();
    }
};

TEST_F(IsrUnitTest, OnDataReceivedPositiveTest)
{
    EXPECT_CALL(GetMock<QueueMock>(), QueuePush(1)).Times(1).WillRepeatedly(Return(0));
    EXPECT_EQ(0, OnDataReceived(1));
}

TEST_F(IsrUnitTest, OnDataReceivedNegativeTest)
{
    EXPECT_CALL(GetMock<QueueMock>(), QueuePush(1)).Times(1).WillRepeatedly(Return(-1));
    EXPECT_CALL(GetMock<GpioMock>(), WriteGpio(10, 1)).Times(1);
    EXPECT_EQ(-1, OnDataReceived(1));
}
...

Well, do you like it? You don't?! I see you are puzzled by the fact that the mock functions are hooked right into the production code, instead of substituting it. Indeed, we used fake implementations while mocking hardware and OS services and it worked just fine. All true, but the HOOKs have several advantages. Let me compare the HOOK and the fake approaches.

  1. Writing and maintaining HOOKs is much easier than fakes. The HOOKs reside in the implementation code, while fakes are located in separate files. Anytime you change function signature, you can update its HOOK right away. Also, with HOOKs, the resulting code is more compact and has better readability.
  2. The HOOKs approach allows you to grow your tests by adding new module tests into the same project. As a result, your testing and production code projects will look quite the same. With the fake functions, you will end up with multiple test projects and only with few modules tested in each of them.
  3. Assuming that every module in your code has its own mock class, you will get a compilation error in the HOOK macro if some function is moved from one compilation unit to another. This way you won't forget to update your mock classes. In case of the fake functions, no error will appear, the testing code will continue working, but the mock class will stop reflecting the real code design.
  4. You can configure HOOKs of a given mock to continue execution of C function after its hook has been hit by the test. This way you may create more complex scenarios, which are sort of integration tests (I do not discuss this feature in this article, but you can find it in the attached code).
  5. The biggest disadvantage of the HOOKs approach is that you can forget to add HOOK to a function. Then two things may happen (a) your mock expectation will not be satisfied and you will find out that HOOK is missing (b) your test will keep executing the production code. Executing the production code in test is not a problem as long as your test is not affected by this code. I believe, this is true in most of the cases (especially if you maintain good encapsulation in your project). ​Anyway, you must acquire some discipline and do not miss HOOKs, but wait… you are embedded C programmer and if you have not learned yet to be well organized, you won't keep your job for long.

One last problem we need to address is initialization of the module global variables. I have already showed, earlier, how to initialize global variables in the test fixture class. The usage of the C module mocks introduces a new challenge: the test must initialize global variables of the mocked module as well. Again, assuming we had made a right design decision of having dedicated mock class for every module, we can put a call to the module's initialization function into the mock constructor.

C++
class QueueMock : public ModuleMock
{
public:
    QueueMock()
    {
        Queue_InitGlobals();
    }
    ...
}

This approach for global variables initialization is a huge advantage of the HOOKs technique. In case of the fake functions, the global variables must be redefined in testing code. Indeed, if your module accesses another module's global variable (not recommended, but sometimes unavoidable), the code won't compile till you add the variable to the testing code. The HOOKs technique solves the problem by bringing the original module's variable into the test project.

Creating test fixture

Till now we have learned how to define and use the mocks in C tests. In this section, I will show how to manage these mocks, so they could be easily plugged into your testing code. We will start with defining templated fixture class, that will get the mocks class as a template parameter. This code is located in the ModuleTest.h file.

C++
extern std::unique_ptr<ModuleMockBase> moduleTestMocks;

template<typename T>
struct ModuleTest : public ::testing::Test
{
    ModuleTest()
    {
        moduleTestMocks.reset(new T);
    }

    virtual ~ModuleTest()
    {
        moduleTestMocks.reset();
    }
};

The ModuleTest class is designed to be a base class for all your test fixture classes. It instantiates the mocks object in its constructor and put it into the moduleTestMocks singleton. This singleton resides in the ModuleTest.cpp file and its content is only valid during the run of a given test. The destructor of the ModuleTest class resets the moduleTestMocks smart pointer and destroys the mocks object. The destruction of the object also forces verification of mock expectations (mock expectations are checked by GooleMock in the mock destructor).

As you might have noticed, the moduleTestMocks singleton holds a pointer to the ModuleMockBase, which we haven't defined yet. The ModuleMockBase definition is located in the ModuleMock.h and it is intended to serve as a base class for all our module mocks. Along with the ModuleMockBase, we also define ModuleMock class that will help us to ensure that there is only one copy of the ModuleMockBase in all our mock classes.  

C++
struct ModuleMockBase
{
    virtual ~ModuleMockBase() = default;
};

struct ModuleMock : public virtual ModuleMockBase
{
};

Now we are ready to define mock classes for our tests. First, we will define a mock class per module and then bundle them into a single mock class to be used with the ModuleTest template.

C++
class GpioMock : public ModuleMock
{
    ...
};

class OscillatorMock : public ModuleMock
{
    ...
};

class QueueMock : public ModuleMock
{
    ...
};

... more mock classes

struct Mocks
    : public ::testing::NiceMock<GpioMock>
    , public ::testing::NiceMock<OscillatorMock>
    , public ::testing::NiceMock<QueueMock>
    , ... // more mocks
{
};

Since all the mock classes inherit from the ModuleMock, we can store it the moduleTestMocks singleton. To improve your tests modularity, I recommend to define a Mocks class per module test and put in it only the mocks that are used by this module.

Finally, we have the TestFixture struct, that is just a syntactic sugaring. I wrote it for the sake of this article, but you might as well skip this class in your code and inherit directly from the ModuleTest<Mocks>.  

C++
struct TestFixture : public ModuleTest<Mocks>
{
};

We are almost done. We have mocks and the singleton that stores them and now we need a convenient way to access them. This could be easily done by the following templated method (located in ModuleTest.h file).

C++
template<typename T>
static T& GetMock()
{
    auto ptr = dynamic_cast<T*>(moduleTestMocks.get());
    if (ptr == nullptr)
    {
        throw std::runtime_error(...);
    }
    return *ptr;
}

The GetMock method allows us retrieval of a mock object of the requested type. As you might recall from the HOOK macro definitions, the HOOK calls to the GetMock method with the MOCK_CLASS macro in the template parameter. The MOCK_CLASS macro have to be defined before including the C file into your test. For example, the QueueUnitTest.cpp file should contain the following code to make the HOOKs work.

C++
#define MOCK_CLASS QueueMock
#include "Queue.c"

All done, We have the mocks, we have the fixture and an easy way to set expectations on our mocks. Storing mocks in singleton global variable was a key part of the design. This works well because, in C, only free functions are mocked and every mocked module presents only one mock object instance in every given test.

Growing your tests

As I have mentioned earlier, the HOOKs approach allows you ongoing addition of the new C module tests into the testing project. In legacy projects, putting all the existing code under the test could be very tedious task. Luckily, you don't have to do it right away. Once you have chosen a module to test, track its dependencies, create stub implementations for the dependent modules and put HOOKs into them. Here is an example of stub.c file content.

C++
#include "MockHooks.h"

void Foo()
{
    MOCK_HOOK_P0(Foo);
}

Then, create a mock class and add an empty unit test for the stub implementation. Below is the content of the StubUnitTest.cpp file.

C++
#include "StubMock.h"

#define MOCK_CLASS StubMock
#include "Stub.c"

Once you will decide to put the real module under test, you will need to replace Stub.c in the include directive with the real module file. You will also need to put the HOOKs into the real module code.

Code sample

In the attached code you will find some goodies that I ddi not describe in this article. So I advise you to read the code before applying the learned techniques on your own project. Of cause, start reading with the README file.

Summary

I hope this article helped you to understand how to utilize the GoogleTest and the GoogleMock frameworks for the embedded C unit testing. The described techniques are not hard to understand, but they require from you pedantic attitude and good understanding of your software design. Try to apply these techniques to some pieces of your code. You will be surprised by how many stupid bugs you will find and, most importantly, it will happen before running the code on your target.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Michael Pan
Software Developer
Israel Israel
No Biography provided

Comments and Discussions

 
QuestionSame namespaces Pin
Member 1402931223-Oct-18 23:01
MemberMember 1402931223-Oct-18 23:01 
QuestionHaving trouble testing my c code Pin
ecat8731-Oct-17 15:30
Memberecat8731-Oct-17 15:30 
GeneralMigrating this project to MSVC 2005 Pin
nagarjunr26-Jan-16 2:15
Membernagarjunr26-Jan-16 2:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.