linux环境编程(1): 实现一个单元测试框架
写在前面
在开发的过程中,大多数人都需要对代码进行测试。目前对于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点:
- 用户只要定义自己的单元测试函数即可。单元测试集的管理,运行,不需要用户额外编写代码,即只要用户定义完单元测试函数,编译之后这个单元测试就自动被测试框架接管了,框架不需要提供额外的单元测试注册接口。
- 在单元测试函数中支持断言。类似gtest的EXCEPT_EQ等宏的功能。
- 不需要其他外部依赖。用户只要有一个框架的头文件和编译好的库文件就可以直接使用。
在说明实现原理之前,我先剧透一下最终的用户是如何使用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): 实现一个单元测试框架的更多相关文章
- python中更人性化的一个单元测试框架:nose2
如果你学过 python 进行自动化测试,你一定使用过 unittest.今天我们要讲的 nose2 是一个高级版本的 unittest.他比 unittest 更容易理解,用起来也更加方便一些. 快 ...
- Linux环境编程相关的文章
Linux环境编程相关的文章 好几年没有接触Linux环境下编程了,好多东西都有点生疏了.趁着现在有空打算把相关的一些技能重拾一下,顺手写一些相关的文章加深印象. 因为不是写书,也受到许多外部因素限制 ...
- Linux 环境编程:dirfd参数 有关解析
背景 在Unix环境编程中,系统提供了很多以at结尾的函数,如openat.fstatat等,而这类函数通常有一个特点,就是形参列表中多了int dirfd 例如: int open(const ch ...
- 实践.Net Core在Linux环境下的第一个Hello World
基础环境和相关软件准备 1.CentOS7.1 64位系统(或者其他CentOS版本的64位系统) 2.WinSCP软件(主要是方便管理和编辑Linux系统的文件) 3.XShell软件(Window ...
- Linux环境编程--waitpid与fork与execlp
waitpid waitpid(等待子进程中断或结束) 表头文件 #include<sys/types.h> #include<sys/wait.h> 定义函数 pid_t w ...
- Linux环境编程进程间通信机制理解
一.Linux系统调用主要函数 二.创建进程 1.创建子进程系统调用fork() 2.验证fork()创建子进程效果 3.系统调用fork()与挂起系统调用wait() 三.模拟进程管道通信 四.pi ...
- Linux环境编程导引
计算机系统硬件组成 总线 贯穿整个系统的一组电子管道称为总线, 分为: 片内总线 系统总线 数据总线DB 地址总线AB 控制总线CB 外部总线 I/O设备 I/O设备是系统与外界联系的通道 键盘鼠标是 ...
- 【Linux环境编程】获取网卡的实时网速
在windows以下.我们能够看到360或者是qq安全卫士的"安全球".上面显示实时的网速情况.那么在linux里面怎样获取网卡的实时网速?事实上原理非常easy,读取须要获取网速 ...
- Linux环境编程之同步(二):条件变量
相互排斥锁用于上锁,条件变量则用于等待.条件变量是类型为pthread_cond_t的变量.一般使用例如以下函数: #include <pthread.h> int pthread_con ...
- 笔记整理:计算CPU使用率 ----linux 环境编程 从应用到内核
linux 提供time命令统计进程在用户态和内核态消耗的CPU时间: [root@localhost ~]# time sleep real 0m2.001s user 0m0.001s sys 0 ...
随机推荐
- day05-JavaScript02
JavaScript02 8.JavaScript函数 JavaScript函数介绍 函数是由事件驱动的,或者当它被调用时,执行的可重复使用的代码 例子 <!DOCTYPE html> & ...
- AR Engine光照估计能力,让虚拟物体在现实世界更具真实感
AR是一项现实增强技术,即在视觉层面上实现虚拟物体和现实世界的深度融合,打造沉浸式AR交互体验.而想要增强虚拟物体与现实世界的融合效果,光照估计则是关键能力之一. 人们所看到的世界外观,都是由光和物质 ...
- AdsStream的使用
本例子是测试ads通信的. 1.首先添加TwinCAT.Ads引用 using System; using System.Collections.Generic; using System.Compo ...
- day01-Tomcat框架分析
引入课程和Maven 1.Maven maven中央仓库:Maven Repository: Search/Browse/Explore (mvnrepository.com) maven仓库是国外的 ...
- Java项目有可能做到所有的代码逻辑均可热部署吗?
前言 首先我们明确下什么叫做热部署,热部署是在不重启java虚拟机的前提下,自动更新class的行为,从而更新整个运行时的逻辑. 在java开发领域,热部署一直是一个难以解决的问题,java虚拟机理论 ...
- Java判断质数/素数的三种方法
介绍 质数:在大于1的整数中,如果只包含1和本身这两个约数,就被称为质数(素数) 解法 解法一:暴力枚举 枚举从2 ~ N的每一个数 实际上不用枚举到N,只需要枚举到√N就行 注意: 不要使用sqrt ...
- ThinkPHP 6.0 RC2 版本发布——架构升级、精简核心
自从5.2版本变更为6.0以来,官方一直致力于优化架构和精简核心,同时也在准备手册和测试工作,在经过近1个月的开发迭代后,官方宣布发布ThinkPHP6.0RC2版本. 主要更新 相比较RC1版本更新 ...
- 刚哥谈架构(八)- 为你的应用选择合适的API
前言: 架构师的主要活动是做出正确的技术决策.选择合适的API是一项重要的技术决策.那么今天就看看API的选择问题. 应用程序编程接口(API)是一种计算接口,它定义了多个软件中介之间的交互.它定义了 ...
- 2、两个乒乓球队,甲队有a,b,c三名队员,乙队有d,e,f三名队员,甲队a不愿和d比赛,c不愿意和d,f比赛,求合适的赛手名单
/*两个乒乓球队,甲队有a,b,c三名队员,乙队有d,e,f三名队员,甲队a不愿和d比赛,c不愿意和d,f比赛,求合适的赛手名单 */ #include <stdio.h> #includ ...
- SQLi
点进去发现是个空白网页,查看源码发现一个login.php的文件,话不多说,直接选择复制然后访问 Url: http://5865f5830d034083b9bbc0dafc6b60a5d5d2309 ...