How do you mock and test the same function in C/C++ with FFF and Google Test?
Asked Answered
S

1

7

I am exploring TDD (Test Driven Development) to test the code that I am writing in C and C++. I have chosen to use Google Test as the unit test framework. I have chosen to use FFF as the mocking framework.

I have written a few tests and run them and it works great. But I have come across an issue that I have not been able to find any reference to online and I was hoping the community could help me (and this would help others as well).

The issue I have is that I would like to write a test for function A1 (see scenario 1 below). Since it calls three more functions (B1, B2 and B3) and they have a lot of dependencies, I decided to mock them so I could test various scenarios that could affect the behavior of function A1. In order for the mocks to work and avoid linker errors (such as "multiple definition of B1") I needed to write "attribute((weak))" before the functions (B1, B2 and B3). So far so good. All works great.

Now, consider scenario 2 below. In this scenario I would like to test function B1 in a separate test. Similarly, I will mock the functions it calls too (C1, C2, C3). However, the problem is that I can't call the "real" B1 function because if I do I will get the mock function I defined previously in the test for A1 function (under scenario 1).

So what should I do in such a case? Thanks.

MOCKING AND TESTING THE SAME FUNCTION

Soulless answered 19/1, 2021 at 15:44 Comment(1)
With dependency Inversion principle you might pass your Mock as interface to your tested functions (no weak symbol required).Pompei
S
11

I have done some more digging and unearthed in the book by James Grenning "Test Driven Development for Embedded C" at least two solutions to this problem.

  1. Link-time Substitution
  2. Function Pointer Substitution

Link-time Substitution:

General summary of this option:
Overall, this seems rather less intuitive to implement and follow and requires a bit of a learning curve. However, the plus side is that you don't need to change anything in your production code which is very important.

In this case, you need to work with makefiles to do the following steps:

  1. Build your production code into a library

  2. Be sure to separate your tests into different files so that tests that need to use a certain function as a mock are in separated from the tests that need to use the original implementation of that same function.

  3. With the make files you will need to make micro builds of parts of your code and finally combine them all together. As an example, for a specific function that you want to both mock and use the original implementation in different tests that are separated in, let's say, two files (test1.cpp contains mock implementation of func A1 and test2.cpp contains original implementation of function A1) you will do:

  • First, build test1.cpp together with the library of the production code and WITHOUT test2.cpp. The mock function will take preference during link time.
  • Second, build test2.cpp together with the library of the production code and WITHOUT test1.cpp. The original implemenation in the library of function A1 will take preference during link time (as it is the only one there).
  • combine the two binaries created together into one executable.

Now these three steps are just a high-level explanation. I know it is not ideal but it is still worth something. I admit I didn't do it myself but I did read about it James Grenning's book and if you would like, he explains it in more detail in Appendix 1 (titled Development System Test Environment) in his book and you may see the makefile structure he used in his book-code-example here: https://pragprog.com/titles/jgade/test-driven-development-for-embedded-c/

Function Pointer Substitution:

General summary of this option:
This is much more intuitive and simple to implement. However, the downside is that it requires a subtle change to your production code where your function is declared and defined.

Let's say you want to mock a function called A1 that is defined in the file Production.c file as:

//Production.c    
int A1(void)
{
  ... original implementation written here
} 

and in Production.h it is declared as:

//Production.h    
int A1(void);

so you may change the function declaration and definition this way:

//Production.c    
int A1_original(void)
{
  ... original implementation written here
}

int (*A1)(void) = A1_original; 

and in Production.h it is declared as:

//Production.h    
extern int (*A1)(void);

#ifdef TDD_ENABLED // use ifdef with TDD_ENABLED which is defined only in unit test project. This is because you want to declare the original implementation function as a public function so that you can freely assign it to the function pointer A1 in the test files.
    int A1_original(void);
#endif

Now, for each test in which you want to use the original function implementation simply call it the same way you would before the change:

A1();

As you can see, this means that also, throughout your production code you don't need to change the way the function is called. This is also true to your testing files.

Now, in case you want to use a mock for this function you simply do the following:

//Test1.cpp
int Fake_A1(void)
{
   ... fake function implementation
}

TEST(test_group_name,test_name)
{
    int (*temp_holder)(void) = A1;  // hold the original pointer in a temp pointer
    A1 = Fake_A1;      // assign A1 to call the mock function 

    ... run all the test here

   A1 = temp_holder; // assign A1 to call the original function back again so that the mock function is used only in the scope of this test
}

Ideally, if you intend to have multiple such tests you would make the assignment to the mock and reassignment to the original function while using a class with Setup() and Teardown() and using a test fixture (TEST_F) like this:

//Test1.cpp
class A1_Func_Test : public ::testing::Test
{
protected:
    int (*temp_holder)(void) = A1;  // hold the original pointer in a temp pointer
    virtual void SetUp()
    {
        A1 = Fake_A1;  // assign A1 to call the mock function
    }

    virtual void TearDown()
    {
        A1 = temp_holder; // assign A1 to call the original function back again so that the mock function exists only in the scope of this test
    }
};

TEST_F(A1_Func_Test , Test1_A1)
{
    write tests here...
}

TEST_F(A1_Func_Test , Test2_A1)
{
    write tests here...
}

How to implement function pointer substitution with FFF Mock Framework:

After following the instructions written above, the same changes should be made to your production code files (Production.c and Production.h). However, for your unit test files you simply do the following in case you want to mock the function (if you want to test the function without mocking it then just call it regularly):

//Test1.cpp
//Using FFF Mocking framework:

DEFINE_FFF_GLOBALS;
FAKE_VALUE_FUNC(int, A1_mock);

int A1_custom(void)
{
   write code here for mock function implementation...
}

TEST(test_group_name,test_name)
{
    int (*temp_holder)(void) = A1;  // hold the original pointer in a temp pointer
    // setting customized mock function for this test
    A1_mock_fake.custom_fake = A1_custom;
    A1 = A1_mock;

    // simple example of a test using the the FFF framework:
    int x;
    x = A1();
    ASSERT_EQ(A1_mock_fake.call_count, 1);

    // assign A1 to call the original function back again so that the mock function exists only in the scope of this test
    A1 = temp_holder;
    RESET_FAKE(A1_mock); // reset all parameters of the mock function used so when used in a subsequent test we will start "clean"
}

Summary

I believe this answers the questions on how to go about and do what I had asked.

Soulless answered 20/1, 2021 at 17:14 Comment(2)
Greatwork. I appreciate and an upvote for you.Peridotite
You don't need int A1_original(void); in the header, do you? Because you're not using it anyway, since you temporary store the original function via the function pointer A1.Mussorgsky

© 2022 - 2024 — McMap. All rights reserved.