在上一篇文章中https://meekrosoft.wordpress.com/2009/11/09/2009/10/04/testing-c-code-with-the-googletest-framework/,我描述了如何开始使用 Google 测试框架测试 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
|
测试这种性质的代码存在一些挑战,但我们也可以使用一些方法来克服它们。
不兼容的函数名称:您不能链接两个主要函数。你需要隐藏一个…
静态内存:这确实会损害测试的独立性。您确实应该为每个测试用例重新初始化所有静态数据,幸运的是,有一种简单的方法可以实现这一点。所有主要的测试框架都有一个测试夹具的概念,它允许您在执行每个测试用例之前调用 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 代码(尤其是嵌入式)时,我经常听到“但是……”
测试 C 代码很难。测试遗留的 C 代码更加困难。但是利用我们在 C 中有限的破坏依赖的语言特性(链接器和预处理器),我们可以完成很多工作。
您可以在 GitHub 上查看原始源代码。