• 使用 GoogleTest 框架对 C 代码进行单元测试


    上一篇文章中icon-default.png?t=M666https://meekrosoft.wordpress.com/2009/11/09/2009/10/04/testing-c-code-with-the-googletest-framework/,我描述了如何开始使用 Google 测试框架测试 C++ 代码。在本文中,我将分享一些测试 C 代码的技巧和窍门。

    那么有什么大不了的,不就是和C++一样吗?

    是的,在某种程度上确实如此,但一如既往,魔鬼在细节中。以下是我们在尝试测试过程代码时面临的一些挑战:

    • 我们无法创建被测代码的实例。这意味着我们不能轻易地为每个测试获取带有初始化数据的新对象。
    • 依赖项是硬编码的。这意味着我们不能使用依赖注入技术来模拟/伪造模块依赖项。
    • 我们不能使用多态来打破依赖关系

    所以这只给我们留下了语言中可用的两个依赖破坏工具:预处理器和链接器。

    需要注意的事项

    静态初始化:在运行每个测试用例之前,您需要能够将数据重置为已知状态。这是将测试彼此隔离的唯一方法。
    全局变量:您的模块是否访问全局变量?您需要为此提供一个虚假的实现。
    硬件访问:在嵌入式系统中,我们经常有内存映射的硬件寄存器访问。您绝对不想在测试中取消引用随机内存地址。一个很好的解决方法是定义一个通用函数来获取给定寄存器的地址。然后,您可以定义此函数的版本以用于测试目的。

    一个例子

    那么在实践中看起来如何呢?假设我们有一个用于控制设备的虚构嵌入式软件应用程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    #include
    #include
     
    #define IOMEM_BASE 0x2FF
    #define VALUE_REG  (IOMEM_BASE + 3)
     
    // This must be a power of 2!
    #define BUFFER_SIZE 8
    #define MAX_ITEMS (BUFFER_SIZE-1)
    static int my_filter[BUFFER_SIZE];
    static int readIdx = 0;
    static int writeIdx = 0;
     
    int filter_len(){ return (BUFFER_SIZE + writeIdx - readIdx) % BUFFER_SIZE; }
     
    void filter_add(int val) {
     my_filter[writeIdx] = val;
     writeIdx = (writeIdx+1) & BUFFER_SIZE-1;
     if(writeIdx == readIdx) readIdx = (readIdx+1) & BUFFER_SIZE-1;
    }
     
    #ifndef TESTING
    int myapp_do_dangerous_io()
    {
     // lets dereference an io mapped register
     // - on the target it is at address IOMEM_BASE + 3
     return *((int *)VALUE_REG);
    }
    #endif
     
    int myapp_get_average(){
     int len = filter_len();
     if(0 == len) return 0;
     int sum = 0;
     for(int i = 0; i < len; i++){
     sum += my_filter[(i+readIdx)%BUFFER_SIZE];
     }
     return sum/len;
    }
     
    int myapp_task()
    {
     // get value from register
     int nextval = myapp_do_dangerous_io();
     
     // add to filter line
     filter_add(nextval);
     
     // return the average value as the next delay
     return myapp_get_average();
    }
     
    int myapp_mainloop()
    {
     for(;;){
     int nextloopdelay = myapp_task();
     sleep(nextloopdelay);
     }
    }
     
    #ifndef TESTING
    int main() {
     printf("!!!Hello World!!!\n");
     return myapp_mainloop();
    }
    #endif

    我们如何测试这种讨厌的东西?

    测试这种性质的代码存在一些挑战,但我们也可以使用一些方法来克服它们。

    无限循环:这些家伙会破坏你有效测试的能力。最好的方法是将任何无限循环的主体移动到它自己的函数调用中。
    危险代码:您在生产环境中对硬件所做的事情在测试环境中可能很危险。在这个例子中,我们有一个内存映射 IO 地址的硬件访问。我们可以通过三种方式来应对困境:
    1. 更改我们取消引用的地址,
    2. 更改我们调用的函数(在链接时)
    3. 隐藏我们在测试期间使用#ifdefs 调用的函数并提供一个测试假(这是我在这里采取的方法)

    不兼容的函数名称:您不能链接两个主要函数。你需要隐藏一个…

    静态内存:这确实会损害测试的独立性。您确实应该为每个测试用例重新初始化所有静态数据,幸运的是,有一种简单的方法可以实现这一点。所有主要的测试框架都有一个测试夹具的概念,它允许您在执行每个测试用例之前调用 SetUp 函数。使用它来初始化您的静态数据。请记住:独立测试是很好的测试!

    一般测试模式

    1. 为您想要存根的依赖项定义假函数
    2. 如果模块依赖于全局(喘气!),您需要定义您的假
    函数 3. 包含您的模块实现(#include module.c)
    4. 定义一个方法将所有静态数据重置为已知状态。
    5. 定义你的测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    #include
     
     // Hide main
     #define TESTING
     // Hide the io function since this will segfault in testing
     int fake_register;
     int myapp_do_dangerous_io()
     {
     return fake_register;
     }
     #include "myapp.c"
     
     class MyAppTestSuite : public testing::Test
     {
     void SetUp(){
     memset(&my_filter, 0, sizeof(my_filter));
     readIdx = 0;
     writeIdx = 0;
     }
     
     void TearDown(){}
     };
     
     TEST_F(MyAppTestSuite, myapp_task_should_return_correct_delay_for_one_element) {
     fake_register = 10;
     EXPECT_EQ(10, myapp_task());
     }
     
     TEST_F(MyAppTestSuite, myapp_task_should_return_correct_delay_for_two_elements) {
     fake_register = 10;
     myapp_task();
     fake_register = 20;
     EXPECT_EQ(15, myapp_task());
     }
     
     TEST_F(MyAppTestSuite, get_average_should_return_zero_on_empty_filter) {
     ASSERT_EQ(0, myapp_get_average());
     }
     
     TEST_F(MyAppTestSuite, addFirstFilterValAddsVal) {
     filter_add(42);
     ASSERT_EQ(42, my_filter[readIdx]);
     }
     
     TEST_F(MyAppTestSuite, addFirstReturnsCorrectAverage) {
     filter_add(42);
     ASSERT_EQ(42, myapp_get_average());
     }
     
     TEST_F(MyAppTestSuite, addTwoValuesReturnsCorrectAverage) {
     filter_add(42);
     filter_add(40);
     ASSERT_EQ(41, myapp_get_average());
     }
     
     TEST_F(MyAppTestSuite, get_average_should_return_average_of_full_filter) {
     for(int i = 0; i < MAX_ITEMS; i++){
     filter_add(i);
     }
     ASSERT_EQ((0+1+2+3+4+5+6)/MAX_ITEMS, myapp_get_average());
     }
     
     TEST_F(MyAppTestSuite, get_average_should_return_average_of_wrapped_filter) {
     for(int i = 0; i < BUFFER_SIZE; i++){
     filter_add(i);
     }
     ASSERT_EQ((1+2+3+4+5+6+7)/MAX_ITEMS, myapp_get_average());
     }
     
     /// ....test buffer operations...
     ...

    这一切都很好,但是<困难的事情>呢?

    在谈论测试 C 代码(尤其是嵌入式)时,我经常听到“但是……”

    • 时间问题。没错,单元测试不能神奇地模拟系统的运行时属性。
    • 中断。这是最后一点的特例,但这是所有开发人员在使用多线程时遇到的相同问题。
    • 位正确操作。如果您在 32 位架构上运行 24 位代码,您将不会看到各种溢出、下溢、位移和算术运算的完全相同的行为。
    • 我不可能测试这个!好吧,有些类的代码根本无法使用单元测试方法进行测试。然而,根据我的经验,这适用于大多数代码库中的极少数。秘诀是尽可能多地排除不可能测试的代码,这样你就不会污染代码库的其余部分。

    概括

    测试 C 代码很难。测试遗留的 C 代码更加困难。但是利用我们在 C 中有限的破坏依赖的语言特性(链接器和预处理器),我们可以完成很多工作。

    您可以在 GitHub 上查看原始源代码。

  • 相关阅读:
    fishhook原理
    【NOI模拟赛】毒药(交互,构造(?))
    信号和槽自动链接20221021
    em,rem,px,rpx的区别与使用
    TechEmpower 21轮Web框架 性能评测 -- C# 的性能 和 Rust、C++并驾齐驱
    【软件测试用例篇】
    从零开始配置vim(21)——会话管理
    【算法】PTA刷题记录
    Vue3获取proxy对象的值而不是引用的方式
    新品发布!华清远见STM32U5开发板重磅推出,从入门到智能手表项目实战~
  • 原文地址:https://blog.csdn.net/u012149181/article/details/126034583