使用 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 <stdio.h>
#include <unistd.h>
 
#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 <gtest/gtest.h>
 
 // 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 上查看原始源代码。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值