如何使用 FFF 和 Google Test 在 C/C++ 中模拟和测试相同的函数?

Eya*_*ber 4 c++ unit-testing mocking googletest fake-function-framework

我正在探索 TDD(测试驱动开发)来测试我用 C 和 C++ 编写的代码。我选择使用Google Test作为单元测试框架。我选择使用 FFF 作为模拟框架。

我已经编写了一些测试并运行它们,效果很好。但我遇到了一个问题,我无法在网上找到任何参考,我希望社区可以帮助我(这也会帮助其他人)。

我遇到的问题是我想为函数 A1 编写一个测试(参见下面的场景 1)。由于它调用了另外三个函数(B1、B2 和 B3),并且它们有很多依赖项,因此我决定模拟它们,以便可以测试可能影响函数 A1 行为的各种场景。为了使模拟工作并避免链接器错误(例如“B1 的多重定义”),我需要在函数(B1、B2 和 B3)之前编写“属性((weak))”。到目前为止,一切都很好。一切都很好。

现在,考虑下面的场景 2。在这种情况下,我想在单独的测试中测试功能 B1。同样,我也会模拟它调用的函数(C1、C2、C3)。然而,问题是我无法调用“真正的”B1 函数,因为如果我这样做,我将获得我之前在 A1 函数测试中定义的模拟函数(在场景 1 下)。

那么遇到这种情况我该怎么办呢?谢谢。

模拟和测试相同的函数

Eya*_*ber 7

我在 James Grenning 的《嵌入式 C 测试驱动开发》一书中做了一些更多的挖掘和挖掘,至少有两个解决这个问题的方法。

  1. 链接时替换
  2. 函数指针替换

链接时替换

此选项的一般摘要:
总体而言,这似乎不太直观地实现和遵循,并且需要一些学习曲线。然而,好的一面是您不需要更改生产代码中的任何内容,这一点非常重要。

在这种情况下,您需要使用 makefile 来执行以下步骤:

  1. 将您的生产代码构建到库中

  2. 请务必将测试分成不同的文件,以便需要使用某个函数作为模拟的测试与需要使用同一函数的原始实现的测试分开。

  3. 使用 make 文件,您将需要对部分代码进行微构建,最后将它们组合在一起。举个例子,对于一个特定的函数,您想要在不同的测试中模拟并使用原始实现,这些测试被分成两个文件(test1.cpp 包含func A1 的模拟实现,test2.cpp 包含 func A1 的模拟实现, test2.cpp包含功能A1)你会做:

  • 首先,将 test1.cpp 与生产代码的库一起构建,但不构建 test2.cpp。在链接期间,模拟函数将优先。
  • 其次,将 test2.cpp 与生产代码的库一起构建,但不构建 test1.cpp。函数 A1 库中的原始实现将在链接时优先(因为它是那里唯一的实现)。
  • 将一起创建的两个二进制文件合并为一个可执行文件。

现在这三个步骤只是一个高级解释。我知道这并不理想,但它仍然有价值。我承认我自己没有这样做,但我确实读过 James Grenning 的书,如果您愿意,他在他的书中的附录 1(标题为开发系统测试环境)中更详细地解释了它,您可能会看到 makefile 结构他在他的书籍代码示例中使用了这里:https ://pragprog.com/titles/jgade/test-driven-development-for-embedded-c/

函数指针替换

此选项的一般总结:
这更加直观且易于实现。然而,缺点是它需要对声明和定义函数的生产代码进行细微的更改。

假设您想要模拟一个名为 A1 的函数,该函数在 Production.c 文件中定义为:

//Production.c    
int A1(void)
{
  ... original implementation written here
} 
Run Code Online (Sandbox Code Playgroud)

在 Production.h 中声明为:

//Production.h    
int A1(void);
Run Code Online (Sandbox Code Playgroud)

所以你可以这样改变函数声明和定义:

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

int (*A1)(void) = A1_original; 
Run Code Online (Sandbox Code Playgroud)

在 Production.h 中声明为:

//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
Run Code Online (Sandbox Code Playgroud)

现在,对于您想要使用原始函数实现的每个测试,只需按照更改前的相同方式调用它即可:

A1();
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这也意味着在整个生产代码中您不需要更改函数的调用方式。您的测试文件也是如此。

现在,如果您想为此函数使用模拟,您只需执行以下操作:

//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
}
Run Code Online (Sandbox Code Playgroud)

Setup()理想情况下,如果您打算进行多个此类测试,则可以在使用带有 和 的类并Teardown()使用测试装置 ( )时对模拟进行分配,然后重新分配给原始函数,TEST_F如下所示:

//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...
}
Run Code Online (Sandbox Code Playgroud)

如何使用 FFF Mock Framework 实现函数指针替换:

按照上面写的说明进行操作后,应对您的生产代码文件(Production.c 和 Production.h)进行相同的更改。但是,对于单元测试文件,如果您想模拟该函数,只需执行以下操作(如果您想测试该函数而不模拟它,则只需定期调用它):

//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"
}
Run Code Online (Sandbox Code Playgroud)

概括

我相信这回答了如何去做我所要求的事情的问题。