写在前面

在开发的过程中,大多数人都需要对代码进行测试。目前对于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环境编程",欢迎持续关注。

linux环境编程(1): 实现一个单元测试框架的更多相关文章

  1. python中更人性化的一个单元测试框架:nose2

    如果你学过 python 进行自动化测试,你一定使用过 unittest.今天我们要讲的 nose2 是一个高级版本的 unittest.他比 unittest 更容易理解,用起来也更加方便一些. 快 ...

  2. Linux环境编程相关的文章

    Linux环境编程相关的文章 好几年没有接触Linux环境下编程了,好多东西都有点生疏了.趁着现在有空打算把相关的一些技能重拾一下,顺手写一些相关的文章加深印象. 因为不是写书,也受到许多外部因素限制 ...

  3. Linux 环境编程:dirfd参数 有关解析

    背景 在Unix环境编程中,系统提供了很多以at结尾的函数,如openat.fstatat等,而这类函数通常有一个特点,就是形参列表中多了int dirfd 例如: int open(const ch ...

  4. 实践.Net Core在Linux环境下的第一个Hello World

    基础环境和相关软件准备 1.CentOS7.1 64位系统(或者其他CentOS版本的64位系统) 2.WinSCP软件(主要是方便管理和编辑Linux系统的文件) 3.XShell软件(Window ...

  5. Linux环境编程--waitpid与fork与execlp

    waitpid waitpid(等待子进程中断或结束) 表头文件 #include<sys/types.h> #include<sys/wait.h> 定义函数 pid_t w ...

  6. Linux环境编程进程间通信机制理解

    一.Linux系统调用主要函数 二.创建进程 1.创建子进程系统调用fork() 2.验证fork()创建子进程效果 3.系统调用fork()与挂起系统调用wait() 三.模拟进程管道通信 四.pi ...

  7. Linux环境编程导引

    计算机系统硬件组成 总线 贯穿整个系统的一组电子管道称为总线, 分为: 片内总线 系统总线 数据总线DB 地址总线AB 控制总线CB 外部总线 I/O设备 I/O设备是系统与外界联系的通道 键盘鼠标是 ...

  8. 【Linux环境编程】获取网卡的实时网速

    在windows以下.我们能够看到360或者是qq安全卫士的"安全球".上面显示实时的网速情况.那么在linux里面怎样获取网卡的实时网速?事实上原理非常easy,读取须要获取网速 ...

  9. Linux环境编程之同步(二):条件变量

    相互排斥锁用于上锁,条件变量则用于等待.条件变量是类型为pthread_cond_t的变量.一般使用例如以下函数: #include <pthread.h> int pthread_con ...

  10. 笔记整理:计算CPU使用率 ----linux 环境编程 从应用到内核

    linux 提供time命令统计进程在用户态和内核态消耗的CPU时间: [root@localhost ~]# time sleep real 0m2.001s user 0m0.001s sys 0 ...

随机推荐

  1. go-zero docker-compose 搭建课件服务(一):编写服务api和proto

    0.转载 go-zero docker-compose 搭建课件服务(一):编写服务api和proto 0.1源码地址 https://github.com/liuyuede123/go-zero-c ...

  2. 知识图谱顶会论文(ACL-2022) ACL-SimKGC:基于PLM的简单对比KGC

    12.(2022.5.4)ACL-SimKGC:基于PLM的简单对比KGC 12.(2022.5.4)ACL-SimKGC:基于PLM的简单对比KGC 摘要 1.引言 2.相关工作 2.1 知识图补全 ...

  3. 1.pytest入门

    一.pytest单元测试框架 概念:单元测试是指在软件开发中,针对软件的最小单位(函数.方法等)进行正确性的检查测试          单元测试框架是自动化测试框架中的组成部分之一           ...

  4. 使用VsCode调试UE5的PuerTs

    使用VsCode调试UE5的PuerTs 1.下载测试的Demo项目 配置PuerTs的步骤这里不赘述. 2.准备工作 2.1 打开项目 正常来说,直接打开项目可以看到如下画面 如果直接点击运行,可以 ...

  5. 现代GPGPU 架构汇总

    本篇是GPGPU 架构汇总的总章,参考的是AMD公布OpenCL 手册,该手册总结了AMD. Nvdia 早年的GPGPU体系架构,以及Opencl 各个API 与硬件结构的映射关系.本篇除了整理这两 ...

  6. chrome工具调试

    项目调试的困境 程序开发总会遇到各种各样的问题,为什么实际结果和预期结果不一致? 这个时候如果能深入程序内部抽丝剥茧去一探究竟再好不过! 而chrome工具是前端开发的杀手锏,经常听到的一句话是: 出 ...

  7. 孙荣辛|大数据穿针引线进阶必看——Google经典大数据知识

    大数据技术的发展是一个非常典型的技术工程的发展过程,荣辛通过对于谷歌经典论文的盘点,希望可以帮助工程师们看到技术的探索.选择过程,以及最终历史告诉我们什么是正确的选择. 何为大数据   "大 ...

  8. 2022-11-05 Acwing每日一题

    本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ...

  9. 2022春每日一题:Day 10

    题目:CF1110E Magic Stones 每次操作 c[i]变成c[i-1]+c[i+1]-c[i],那么显然,c[1]和c[n]是不会改变的,因此只要c[1]和t[1],c[n]和t[n]不相 ...

  10. JDK源码分析实战系列-PriorityQueue

    完全二叉树 一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下.从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树 ...