分类: C/C++
2008-01-01 16:58:41
在Java、C++和C#等高级语言的单元测试正进行的如火如荼的时候,C好像做了看客,冷清的躲在了一个不起眼的角落里。C并不是没有单元测试工具,像Check和CUnit这样的工具也很有名气,只是和大名鼎鼎的JUnit比起来,还是显得有些英雄气短。很多大型的C项目,如APR等都没有使用像Check、CUnit这样通用的单元测试框架,而是另起炉灶自己编写。其实编写一个仅能满足单个项目需要的C单元测试工具包并非难事。在部分参考APR的ABTS的前提下,我们也来设计一套自己的简单的C语言单元测试包。
鉴于减少复杂性,我们的目标仅仅是设计和实现一套能在单进程、单线程下工作良好的C单元测试包,我们暂且将之命名为CUT - C Unit test Toolkit。
1、CUT涉及的术语解释
曾经接触过多个有名的单元测试框架如JUnit、CppUnit、TestNG等,它们在对单元测试某些概念的理解上并不是全都一样的。这里我们也有我们自己的定义。
a) 一个逻辑unit test包含至少一个或者多个suite;
b) 一个suite包含至少0或者多个test case;
c) 每个test case中至少包含1个或者多个“断言类”语句。
2、CUT预告片
其实每设计一个程序之前自己都会考虑该提供给用户怎样的东东呢?下面是应该是CUT的经典用法:
cut_ts_t *suite = NULL;
CUT_TEST_BEGIN("classic usage of CUT");
CUT_TS_INIT(suite);
CUT_TC_ADD(suite, "test case: tc_add", tc_add);
CUT_TC_ADD(suite, "test case: tc_sub", tc_sub);
CUT_TS_ADD(suite, my_setup, my_teardown);
CUT_TEST_RUN();
CUT_TEST_REPORT();
CUT_TEST_END();
3、CUT的组织结构
从上面的经典用法中也可以看出我们的CUT的组织是这样的:
Test
|
|
+-------------+
TS-1 ... TS-N
| |
| |
+-------+ ... +--------+
TC-1 TC-N TC-1 TC-N
其中:TS - Test Suite,TC - Test Case
4、CUT接口设计与实现
在“预告片”中我们已经暴露了大部分CUT的重要接口,在下面我们将伴随着实现逐一说明。另外在CUT的实现中我们使用了APR RING技术,不了解APR RING的可以参见我的上一篇Blog“APR分析-环篇”。
1) 主要数据结构
typedef void (*tc_func)(cut_tc_t *tc); /* Test Case标准原型函数指针,所有的Test Case都应该符合这个原型 */
typedef void (*fixture_func)(); /* 用于suite环境建立和拆除的func原型 */
/* Test Case数据结构 */
typedef struct cut_tc_t {
APR_RING_ENTRY(cut_tc_t) link;
char name[CUT_MAX_STR_LEN+1];
tc_func func;
int failed;
} cut_tc_t;
typedef APR_RING_HEAD(cut_tc_head_t, cut_tc_t) cut_tc_head_t;
/* Test Suite数据结构 */
typedef struct cut_ts_t {
APR_RING_ENTRY(cut_ts_t) link;
cut_tc_head_t tc_head;
int failed; /* 失败用例总数 */
int ran; /* 运行用例总数 */
fixture_func sf; /* setup func */
fixture_func tf; /* teardown func */
} cut_ts_t;
typedef APR_RING_HEAD(cut_ts_head_t, cut_ts_t) cut_ts_head_t;
/* 逻辑单元测试数据结构 */
typedef struct cut_test_t {
char name[CUT_MAX_STR_LEN+1];
cut_ts_head_t ts_head;
} cut_test_t;
2) CUT_TEST_BEGIN和CUT_TEST_END
这两者分别是一个逻辑Test的开始与结束。我们在CUT_TEST_BEGIN建立好我们的内部数据结构,其唯一宏参数用来加强可读性,在CUT_TEST_END中释放在测试过程中获取的系统资源。其实现如下:
#define CUT_TEST_BEGIN(desc) \
cut_test_t *_cut_test = NULL; \
_cut_test = malloc(sizeof(cut_test_t)); \
if (_cut_test == NULL) { \
return errno; \
} \
memset(_cut_test, 0, sizeof(cut_test_t)); \
APR_RING_INIT(&(_cut_test->ts_head), cut_ts_t, link); \
strncpy(_cut_test->name, desc, CUT_MAX_STR_LEN)
#define CUT_TEST_END() do { \
/* 这里遍历Ring,释放其他相关内存,这里限于篇幅未写出 */
if (_cut_test != NULL) { \
free(_cut_test); \
} \
} while(0)
3) CUT_TS_ADD和CUT_TC_ADD
前者负责向一逻辑单元测试中添加Test Suite,后者则负责向一个Test Suite中添加测试用例。在CUT中,每个Test Suite依赖两个Fixture Function- setup和teardown。setup用于建立测试环境,比如打开某文件,获得文件句柄供该Test Suite中的若干Test Case使用;而teardown则用来做后处理,释放setup以及在众多Test Case执行时分配的资源,比如上面关闭提到的文件句柄。
在实现CUT的Test Suite时,实际上加了一个对用户使用的限制,那就是CUT负责管理Test Suite的内存分配,说限制也好我觉得倒是给用户提供了一种方便。这两个宏的实现如下:
#define CUT_TEST_SUITE_INIT(suite) do { \
if (suite == NULL) { \
suite = malloc(sizeof(cut_ts_t)); \
if (suite == NULL) { \
return errno; \
} \
} \
memset(suite, 0, sizeof(cut_ts_t)); \
APR_RING_INIT(&(suite->tc_head), cut_tc_t, link); \
suite->ran = 0; \
suite->failed = 0; \
} while(0)
#define CUT_TS_ADD(suite, f1, f2) do { \
APR_RING_ELEM_INIT(suite, link); \
suite->sf = f1; \
suite->tf = f2; \
APR_RING_INSERT_TAIL(&(_cut_test->ts_head), suite, cut_ts_t, link); \
} while(0)
#define CUT_TC_ADD(suite, desc, f1) do { \
cut_tc_t *tc = NULL; \
tc = malloc(sizeof(cut_tc_t)); \
if (tc == NULL) { \
return errno; \
} \
memset(tc, 0, sizeof(cut_tc_t)); \
strncpy(tc->name, desc, CUT_MAX_STR_LEN); \
tc->func = f1; \
APR_RING_ELEM_INIT(tc, link); \
APR_RING_INSERT_TAIL(&(suite->tc_head), tc, cut_tc_t, link); \
} while(0)
4) CUT_TEST_RUN和CUT_TEST_REPORT
这两个宏的作用分别是运行所有逻辑单元测试中的测试用例和报告测试情况,在这里CUT_TEST_REPORT输出形式较为简单,只是打印出此次单元测试运行用例总数和失败的用例数。当然要丰富其输出形式,让用户更快更早定位哪个测试用例失败也并不难,只需对CUT的实现稍作修改即可,这里仅是抛砖引玉。具体可参见成熟的工具的输出形式,如CUnit等。
#define CUT_TEST_RUN() do { \
cut_ts_t *ts = NULL; \
cut_tc_t *tc = NULL; \
%