You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

182 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 29 | 尝试升级(下):“链表”知识在测试框架中的应用
你好,我是胡光,欢迎回来。
上节课中,我们通过参考 gtest 的输出,完善了我们自己的测试框架的输出信息,也就是添加了测试用例的名称、运行结果以及运行时间。并且,我提到了在一般情况下,项目中的功能开发原则:**功能迭代,数据先行**。就是要开发新的功能之前,我们应该先考虑清楚实现这部分功能相关的数据,在系统中的存储与使用的情况,只有这样,才能更好地完成功能的实现与迭代优化。
今天迎来我们整个测试框架项目的最后一节课。这节课的目的,一是对前几节课内容进行一个总结,二是向你说明我们现在开发的测试框架代码,其实还有很多优化的空间。至于这个优化空间是什么呢?这次我将带着你结合之前学习的“链表”知识,对测试框架进行一个具体的优化改进。
关于测试框架的优化,是一个不断学习的过程。在这个过程中,你深刻体会到“知不足,然后能自反也”这句话的含义。就是在优化代码的过程中,你会发现自己的不足,然后努力提高自己的能力去弥补不足;当你提升了自己之后,你又会看到自己在其他方面的不足,进而继续提高自己。
好了,废话不多说,我们正式开始今天的学习。
## 揭晓答案EXPECT\_EQ 宏究竟是如何实现的
在对测试框架进行优化之前呢,我先来回答一下,可能困扰了你两节课的一个问题:就是 EXPECT\_EQ 宏究竟是如何实现的?这个问题的答案呢,我给出一个可行的实现,仅供参考。
首先EXPECT\_EQ(a, b) 在ab 两部分值相等的时候,不会产生额外的输出信息,而当 ab 两部分不相等的时候,就会输出相应的提示信息。如下所示:
```
gtest_test.cpp:25: Failure
Expected equality of these values:
is_prime(4)
Which is: 1
0
```
这段输出信息,对应的是源代码中的 “EXPECT\_EQ(is\_prime(4), 0); ”的输出。如你所见,第 1 行的输出内容包含了源文件名gtest\_test.cppEXPECT\_EQ 宏所在的代码位置25以及一个提示结果Failure
接下来的信息,你自己就可以看懂了,就是关于 EXPECT\_EQ 传入两部分的值。对于函数调用部分EXPECE\_EQ 会输出这个函数的调用形式及返回值信息,也就是输出中的 “is\_prime(4)”“Which is: 1” 这段内容。而对于数值信息,只会输出数值信息本身,也就是输出信息中第 5 行的那个 0。
实际上,要想在宏中实现类似于这种根据传入参数类型,选择输出形式的功能,对于现在的你来说可能有点困难。所以,我们可以重新设计一种输出形式,只要能够清晰地展示错误信息就可以。
重新设计的输出提示,如下所示:
```
gtest_test.cpp:25: Failure
Expected (is_prime(4) == 0):
Which is: (1 == 0)
```
修改完以后的输出信息,你可以看到,第 2 行就是传入 EXPECT\_EQ 宏两部分的比较,第 3 行是这两部分实际输出值的比较。
重新设计了输出信息以后,就可以来看看 EXPECT\_EQ 宏的实现了:
```
#define EXPECT(a, b, comp) { \
__typeof(a) val_a = (a), val_b = (b); \
if (!(val_a comp val_b)) { \
printf("%s:%d: Failure\n", __FILE__, __LINE__); \
printf("Expected (%s %s %s):\n", #a, #comp, #b); \
printf(" Which is: (%d %s %d)\n", val_a, #comp, val_b); \
test_run_flag = 0; \
} \
}
#define EXPECT_EQ(a, b) EXPECT(a, b, ==)
#define EXPECT_LT(a, b) EXPECT(a, b, <)
#define EXPECT_GT(a, b) EXPECT(a, b, >)
#define EXPECT_NE(a, b) EXPECT(a, b, !=)
```
在这段实现中,你会发现,我们不仅实现了 EXPECT\_EQ还额外实现了EXPECT\_LT、EXPECT\_GT、EXPECT\_NE 等用于比较的宏。其中LT 是英文 little 的缩写是判断小于关系的GT 是 great 的缩写是判断大于关系的NE 是 not equal 的缩写,是判断不等于关系的。而这些所有的宏,都是基于 EXPECT 宏实现的。
我们将用于比较的运算符,当作参数传递给 EXPECT 宏。有了 EXPECT 宏以后你就可以参考代码中的第1013行的内容轻松地扩展出用于小于等于或者大于等于的宏了。由于 EXPECT 宏的实现,全都是我们之前学习过的知识点,所以在这里我就不再赘述了,你可以自行阅读文稿中的代码。
## 用链表存储测试用例
看完了 EXPECT 宏的参考实现以后,整个测试框架的基础功能,就算是彻底搭建完成了。
接下来,我们再重新审视下面这段函数指针数组 test\_function\_arr 的代码设计,来思考一下这个测试框架中还有没有可以优化的地方。
```
struct test_function_info_t {
test_function_t func; // 测试用例函数指针,指向测试用例函数
const char *name; // 指向测试用例名称
} test_function_arr[100];
int test_function_cnt = 0;
```
这段代码中,我们使用了数组来定义存储测试函数信息的存储区,这个数组的大小有 100 位,也就是说,最多可以存储 100 个测试用例函数信息。
那我们来思考一个问题:要是有程序中定义了 1000 个测试用例,怎么办呢?毕竟,对于中型项目开发来说,定义 1000 个测试用例,可不是什么难事儿。这个时候,你可能会说,那简单啊,数组大小设置成 10000 不就行了。
但是你要明白这种设计尽管简单粗暴且有效可它一点儿程序设计的美感都没有。什么意思呢就是当我们为测试用例准备了10000个数组空间的时候可能在真正的开发过程中只定义了 2 个测试用例,这就会浪费掉 9998 个数组空间。
更形象地描述这种行为的话,这种设计方式很像计划经济,计划多少用多少。同时,它的弊端也很明显,一旦计划不好,要不是造成空间浪费,要不就是资源紧张。
所以,我们应该尝试着从“计划经济”向“市场经济”转变一下,可不可以转变成想用多少就生产多少。那应该怎么做呢?
我们知道,在程序中数组的空间大小,是需要提前计划出来的。但是有一种结构的空间,是可以动态增加或减少的,那就是我们之前讲过的“**链表**”结构。你想一下,如果我们把一个一个的测试函数信息,封装成一个一个的链表节点,每当增加一个测试用例的时候,就相当于向整个链表中插入一个新的节点。此时,用链表实现的存储测试函数信息的结构,它所占空间大小就和实际测试用例的数量成正比了。这就是我说的用多少,就生产多少。
下面,我们就来说说具体怎么做。
第一步,我们需要改变 test\_function\_info\_t 的结构定义,也就是把原先存储测试用例函数信息的结构体类型,改装成链表结构。最简单的方法,就是在结构体的定义中,增加一个指针字段,指向下一个 test\_function\_info\_t 类型的数据,代码如下所示:
```
struct test_function_info_t {
test_function_t func; // 测试用例函数指针,指向测试用例函数
const char *name; // 指向测试用例名称
struct test_function_info_t *next;
};
struct test_function_info_t head, *tail = &head;
```
可以看到,我们给 test\_function\_info\_t 结构体类型增加了一个链表中的 next 字段,除此之外,我们还定义了一个虚拟头节点 head 和一个指针变量 tail。这里你需要注意head 是虚拟头节点,后续我们会向 head 所指向链表中插入链表节点tail 指针则指向了整个链表的最后一个节点的地址。
第二步,在准备好了数据存储结构以后,需要改写的就是函数注册的逻辑了。在改写 TEST 宏中的注册函数逻辑之前呢,我们先准备一个工具函数 add\_test\_function这个工具函数的作用就是根据传入的参数新建一个链表节点并且插入到整个链表的末尾
```
void add_test_function(const char *name, test_function_t func) {
struct test_function_info_t *node;
node = (struct test_function_info_t *)malloc(sizeof(struct test_function_info_t));
node->func = func;
node->name = name;
node->next = NULL;
tail->next = node;
tail = node;
return ;
}
```
好了, add\_test\_function 工具函数准备好之后,我们正式来改写 TEST 宏中注册函数的逻辑。其实难度也不大,也就是要求注册函数调用 add\_test\_function 函数,并且传入相关的测试用例的函数信息即可:
```
#define TEST(test_name, func_name) \
void test_name##_##func_name(); \
__attribute__((constructor)) \
void register_##test_name##_##func_name() { \
add_test_function(#func_name "." #test_name, \
test_name##_##func_name); \
} \
void test_name##_##func_name()
```
最后一步,处理完了数据写入的过程以后,来让我们修改一下使用这份数据的代码逻辑,那就是 RUN\_ALL\_TESTS 函数中的相关逻辑。之前RUN\_ALL\_TESTS 函数中,循环遍历数组中的每一个测试用例,并且执行相关的测试用例函数,对这一部分,修改成针对于链表结构的遍历方式即可,代码如下所示:
```
int RUN_ALL_TESTS() {
struct test_function_info_t *p = head.next;
for (; p; p = p->next) {
printf("[ RUN ] %s\n", p->name);
test_run_flag = 1;
long long t1 = clock();
p->func();
long long t2 = clock();
if (test_run_flag) {
printf("[ OK ] ");
} else {
printf("[ FAILED ] ");
}
printf("%s", p->name);
printf(" (%.0lf ms)\n\n", 1.0 * (t2 - t1) / CLOCKS_PER_SEC * 1000);
}
return 0;
}
```
这样,我们就彻底完成了测试用例函数信息存储部分的“**链表**”改造过程。
对于上面的这份代码实现,你会发现,链表节点空间是通过 malloc 函数动态申请出来的,可在我们的程序中,并没有对这些空间使用 free 进行释放,如果你想让这个程序对空间的申请与回收做到有始有终,变得更加干净,那应该怎么办呢?
这里你可以借助 `__attribute__`((destructor)) 的功能,之前我们介绍了一个`__attribute__`((constructor))的作用是让函数先于主函数执行而destructor 就是使函数在主函数结束以后才执行的函数特性设置。有了这个特性设置,你可以实现一个函数,专门用来销毁测试函数链表所占存储空间,这样在逻辑上,你的程序会变得更完美。当然,你即使不这么做,也不会影响到原有的程序功能的正确性。
## 项目小结
至此,我们关于测试框架开发的内容,就算是告一段落了。
从26讲到29讲我们经历了一个项目从 0 到 1 的过程,继而又完成了项目从 1 到 1.5 的升级。所谓从 0 到 1 就是项目从最初的想法变成代码的过程,从 1 到 1.5 就是我们对于代码的优化过程。这是一个追求极致、不断优化项目的过程。
关于测试框架开发的讲解内容虽然结束了,但我希望这几节课可以成为你优化这份代码的一个起点。日后,你可以选择增加额外的功能,修改实现的架构,甚至是使用不同的语言重新进行实现,哪怕是一个小小的改动,都是值得称赞的。
在下节课,我将指导你完成一个简易的计算器程序,也算是给我们这个课程一个圆满的结束。
好了,今天就到这里了,我是胡光,我们下一节课见。