写在前面

在开发的过程中,大多数人都需要对代码进行测试。目前对于c/c++项目,可以采用google的gtest框架,除此之外在github上搜索之后可以发现很多其他类似功能的项目。但把别人的轮子直接拿来用,终究比不过自己造一个同样功能的轮子更有成就感。作为“linux环境编程”系列文章的第一篇,本篇文章记录了如何用较少的代码实现一个可用的单元测试框架,这个测试框架将一直在后续系列文章中的代码实例环节使用,并且在使用过程中不断完善和改进。相比于文字说明,我相信有人更喜欢直接看代码实现,所以这里先放一个github传送门

需求来源

先给项目取个名字吧,毕竟命名是最困难的环节了:P. 这个测试框架一句话可以概括为"a simple unit test framework for c programming language", 所以就叫它"cutest"好了,希望不要有人把前缀"cu"和"cuda"联系起来。

接下来我把自己切换到用户的角度,说说我希望自己如何使用cutest。首先,测试的对象对我来说是一个个独立的函数,我希望框架提供一种让我定义单元测试函数的方法,定义完成之后我作为用户的任务就完成了,至于如何让框架知道有一个新的单元测试加入进来了,那不是我这个用户应该关心的,我甚至不打算写main函数,然后在main函数里告诉cutest去运行所有注册进去的测试。我想要的只是定义单元测试,编译链接,运行得到结果。总结起来,需求有以下3点:

  1. 用户只要定义自己的单元测试函数即可。单元测试集的管理,运行,不需要用户额外编写代码,即只要用户定义完单元测试函数,编译之后这个单元测试就自动被测试框架接管了,框架不需要提供额外的单元测试注册接口。
  2. 在单元测试函数中支持断言。类似gtest的EXCEPT_EQ等宏的功能。
  3. 不需要其他外部依赖。用户只要有一个框架的头文件和编译好的库文件就可以直接使用。

在说明实现原理之前,我先剧透一下最终的用户是如何使用cutest编写单元测试的:

/*test1.c*/

/*include cutest header file*/
#include "cutest.h" /*define a new test suit with the name 'test_suit1'*/
CUTEST_SUIT(test_suit1) /*define first test case in test_suit1*/
CUTEST_CASE(test_suit1, test1) {
int a = 10;
int b = 10;
CUT_EXPECT_EQ(a, b);
} /*define the second test case in test_suit1*/
CUTEST_CASE(test_suit1, test2) {
int a = 10;
int b = 20;
CUT_EXPECT_GT(a, b);
}

编译这个test1.c文件,然后在shell中运行就可以得到结果了:

$ ./test1
cutest summary:
[test_suit1] suit result: 1/2
[test_suit1::test2] case result: Fail
[test_suit1::test1] case result: Pass

用户的的单元测试可以放在多个文件中,但是定义单元测试函数的步骤是不变的,用户的所有测试都会在运行结束之后得到一个汇总的统计信息。可以看到,cutest使用起来还是很简单方便的。

如何实现

聊完了用户的想法,需要考虑如何实现了。

基本数据结构

在cutest中我定义了两种重要的数据结构:

  • test_case:

    最基本的单元测试。对应用户实现的单元测试函数,test_case中还记录了测试的名字,测试的最终运行结果。

  • test_suit:

    单元测试集合。用于管理一组test_case,理论上应该把一组相关性较大的test_case放在同一个集合中。测试框架中可以同时存在多个test_suit。

为了管理用户定义的多个单元测试, 我采用了链表数据结构。test_case本身是一个链表,通过当前的test_case可以找到下一个test_case;每个test_suit中包含一个test_case的链表,这个链表就是这个test_suit管理的全部单元测试,test_suit自身也是一个链表,通过这个链表指针可以找到下一个test_suit。两个结构体的定义如下:

typedef struct test_case {
char *test_name;
test_func test_func;
struct test_case *test_case_next;
int test_result;
} test_case; typedef struct test_suit {
char *test_suit_name;
test_case *test_case_list;
int test_case_total;
int test_case_passed;
struct test_suit *test_suit_next;
} test_suit;

在test_case中除了维护链表,还记录了测试名字和结果;同理,test_suit额外记录了当前测试集合的名字,包含的全部单元测试个数,以及测试通过的个数,这些信息将用于最后的测试结果统计。

如何实现单元测试的自动注册

为了管理全部的单元测试,还需要一个test_suit的头节点,当用户代码定义了新的test_suit的时候,会自动的加入到这个头节点上;同时,当用户在新的test_suit上定义新的test_case时,需要能够自动加入到该test_suit的test_case链表上。下面的一个关键问题就是如何在用户定义test_suit或者test_case的同时自动注册到cutest框架中。这里用到的方式是通过编译器给一个函数添加"construtor"属性,这样在程序运行时,具有"constructor"属性的函数会由c库调用,编程者可以定义多个"constructor"函数,这些函数会在main函数开始之前被调用,利用这个特性我们可以在“constructor”函数中实现test_suit以及test_case的注册,这样就实现了让cutest框架管理这些用户数据。下面以CUTEST_SUIT宏的实现为例,说明test_suit的注册是如何完成的:

/* CUTEST_SUIT宏的功能由两个辅助宏实现.
* __DEFINE_CUTEST_SUIT定义个一个test_suit变量,并对其成员进行了初始化;
* __REGISTER_CUTEST_SUIT定义了一个"constructor"函数, 在该函数中, 上一步定义的test_suit被添加到了test_suit的链表中;
*/
#define CUTEST_SUIT(suit_name) \
__DEFINE_CUTEST_SUIT(suit_name) \
__REGISTER_CUTEST_SUIT(suit_name) /*定义test_suit变量*/
#define __DEFINE_CUTEST_SUIT(suit_name) \
test_suit __CUTEST_SUIT_NAME(suit_name) = __CUTEST_INIT_SUIT(suit_name); /*定义"constructor函数, 完成test_suit的注册"*/
#define __REGISTER_CUTEST_SUIT(suit_name) \
void __attribute__((constructor)) \
__cutest_register_test_suit_##suit_name() { \
__CUTEST_INSERT_TEST_SUIT(suit_name); \
}

上述宏的实现用到的test_suit变量名以及"constructor"函数名是根据用户传递进来的宏参数拼接而成,只要保证用户的参数正确就不会出现变量重复定义的问题。每次用户定义一个新的test_suit就会自动定义出一个不同名字的"constructor"函数,而这些函数运行时的调用顺序是有c库决定的,但不管调用顺序如何只要不会并发调用,对cutest来说就是无关紧要的,因为调用顺序只影响链表节点的顺序,但cutest并未承诺保证单元测试的执行顺序。

对于test_case的注册和管理采用了相同的技术实现,这里不再重复。

如何获取测试结果

对于每个单元测试函数,其函数签名实际上是void (*) (void), 在注册test_case的时候,由宏定义的"constructor"函数默认会将test_case的test_result置为CUTEST_PASS,即默认情况下单元测试的状态是通过。如果用户需要把当前的测试标记为FAIL,可以在单元测试函数中使用CUT_FAIL宏。在代码中可以这样实现:

CUTEST_SUIT(test_suit2)

CUTEST_CASE(test_suit2, test2) {
CUT_EXPECT_EQ(10, 10);
CUT_FAIL();
}

当CUT_FAIL宏会设置当前测试的状态,并且让单元测试函数return。除了CUT_FAIL,cutest还提供了其他用于比较的宏,比如CUT_EXPECT_EQ, CUT_EXPECT_NE, CUT_EXPECT_LT等,这样的设计延续了其他测试框架的使用方式,用户的使用成本更低。cutest提供的断言类宏最终的实现都是基于一个宏CUT_EXPECT_TRUE,这个宏的实现如下:

#define CUT_EXPECT_TRUE(c)                                                     \
do { \
if (!(c)) { \
__cutest_current_test_case__->test_result = CUTEST_FAIL; \
__cutest_current_test_suit__->test_case_passed -= 1; \
return; \
} \
} while (0)

如果条件为假,该宏会把当前test_case的结果设为FAIL,把对应test_suit中PASS状态的测试数量减1,最后return。其他宏的实现只需要构造合适的条件c,传递给CUT_EXPECT_TRUE即可。

如何做到可以不写main函数

这是因为在cutest库中已经定义了一个main函数,在main函数中代替用户完成了运行全部单元测试的工作。

int main(int argc, char **argv) {
cutest_run_all();
return 0;
}

当用户程序和libcutest.so进行链接之后,生成的可执行程序就有了main函数,当然用户也可以自己重新定义一个main函数,去执行更复杂的功能,如果没有其他需求就可以不必定义main函数。

写在最后

至此,cutest实现过程中用到的一些技术细节都已经介绍完毕。对于我目前的使用,其功能已经足够了,但还是有些需要完善的地方,比如:

  • 在多线程环境下是否能够正常使用?当用户自己定义main函数时,使用多个线程调用cutest_run_all函数会存在哪些问题?
  • 能否做到跨平台?给函数添加"constructor"属性,不知道在WIndows上是否可行,不过这一点目前不是重点。
  • 目前的CUT_EXPECT_XX宏,当条件不成立时处理的逻辑是标记测试为FAIL并从单元测试函数return。这样的逻辑对于用户需要在return之前释放一些资源的情况是不适用的,这个问题需要后续解决。

最后,再放一个代码传送门,期待各位提出的建议。后续会发布系列文章"linux环境编程",欢迎持续关注。

最新文章

  1. tcpreplay,tcprewrite的使用---张子芳
  2. iframe操作
  3. python走起之第三话
  4. 【LeetCode OJ】Word Break II
  5. iOS LaunchScreen启动图设置
  6. Concurrent inserts on MyISAM and the binary log
  7. Windows Service的安装卸载 和 Service控制(转)
  8. IntentService 串联 按顺序执行(此次任务执行完才执行下一个任务)
  9. 基础:c++中引用与java中的引用
  10. iOS开发——手机号,密码,邮箱,身份证号,中文判断
  11. Spring AOP实现方式三【附源码】
  12. 牛刀小试、用SharePoint 实现请假管理功能
  13. centos下网络代理服务器的配置
  14. LR11安装注意事项
  15. 数据库基础——(SQLserver)约束
  16. socket__服务端于客户端
  17. idea代码快捷
  18. js在前端json字符串和对象互相转化
  19. 【dfs】p1731 生日蛋糕
  20. 初入爬虫(java)

热门文章

  1. python信息检索实验之向量空间模型与布尔检索
  2. Ansible 批处理实战
  3. 题解 P6745 『MdOI R3』Number
  4. photoshop 2021 for mac安装教程,亲测可用!!!
  5. Pthread 并发编程(一)——深入剖析线程基本元素和状态
  6. JS数据结构与算法-数组结构
  7. Dive into TensorFlow系列(1)-静态图运行原理
  8. ui自动化测试数据复原遇到的坑——1、hibernate输出完整sql
  9. 【Java并发010】使用层面:发令枪CountDownLatch全解析
  10. x=x+=x-=x-x;