1, 简介
CuTest 是一款微小的 C 语言单元测试框, 是我迄今为止见到的最简洁的测试框架之一, 只有 2 个文件, CuTest.c 和 CuTest.h, 全部代码加起来不到一千行. 麻雀虽小, 五脏俱全, 测试的构建, 测试的管理, 测试语句, 都全部包含在内.
2,CuTest 剖析
2.1 断言
一个测试 case 是否通过落到代码实处, 就是对测试值与期待值之间进行比较, 这就要用到断言.
- #define CuAssertStrEquals(tc,ex,ac) CuAssertStrEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac))
- #define CuAssertStrEquals_Msg(tc,ms,ex,ac) CuAssertStrEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac))
- #define CuAssertIntEquals(tc,ex,ac) CuAssertIntEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac))
- #define CuAssertIntEquals_Msg(tc,ms,ex,ac) CuAssertIntEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac))
- #define CuAssertDblEquals(tc,ex,ac,dl) CuAssertDblEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac),(dl))
- #define CuAssertDblEquals_Msg(tc,ms,ex,ac,dl) CuAssertDblEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac),(dl))
- #define CuAssertPtrEquals(tc,ex,ac) CuAssertPtrEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac))
- #define CuAssertPtrEquals_Msg(tc,ms,ex,ac) CuAssertPtrEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac))
- ......
- ......
以数字测试为例 CuAssertIntEquals, 其实现为:
- void CuAssertIntEquals_LineMsg(CuTest* tc, const char* file, int line, const char* message,
- int expected, int actual)
- {
- char buf[STRING_MAX];
- if (expected == actual) return;
- sprintf(buf, "expected <%d> but was <%d>", expected, actual);
- CuFail_Line(tc, file, line, message, buf);
- }
如果测试成功, 则会安静的进行下一步, 由 return 返回此函数.
大部分的测试框架的哲学和 linux 哲学很像, 小即是美, 少就是好, 没有异常下不会打扰用户.
而万一出现错误, 则会保存错误信息, 还有文件路径 / 文件名 / 函数名, 及行号.
- sprintf(buf, "expected <%d> but was <%d>", expected, actual);
- CuFail_Line(tc, file, line, message, buf);
继续深入, 上面函数实现了: 拼接错误消息到 string, 然后传递给 CuFailInternal 函数. 很容易从 CuFailInternal 函数名发现, 这个函数才是真正的错误返回的核心.
1)把函数名和行号, 追加到用户错误消息的字符串后面. 由 CuStringInsert 语句实现.
2)错误标志, tc->failed 置位.
3)完整的错误消息引用赋值给测试的消息指针.
4)返回, 长跳转.
- void CuFail_Line(CuTest* tc, const char* file, int line, const char* message2, const char* message)
- {
- CuString string;
- CuStringInit(&string);
- if (message2 != NULL)
- {
- CuStringAppend(&string, message2);
- CuStringAppend(&string, ":");
- }
- CuStringAppend(&string, message);
- CuFailInternal(tc, file, line, &string);
- }
- static void CuFailInternal(CuTest* tc, const char* file, int line, CuString* string)
- {
- char buf[HUGE_STRING_LEN];
- sprintf(buf, "%s:%d:", file, line);
- CuStringInsert(string, buf, 0);
- tc->failed = 1;
- tc->message = string->buffer;
- if (tc->jumpBuf != 0) longjmp(*(tc->jumpBuf), 0);
- }
到这里, 一个错误的测试就会从 longjmp 返回.
2.2 测试的组织
无论设计多么精妙的测试, 都需要一个一个的逻辑测试函数, 这就是测试 case. 比如下面的测试 case.
待测函数原型:
int AddInt(int a, int b);
测试用例:
- void test_add(CuTest* tc)
- {
- CuAssert(tc, "\r\ntest not pass", 2 == AddInt(1,0);
- }
- CuSuite* TestAdd(void)
- {
- CuSuite* suite = CuSuiteNew();
- SUITE_ADD_TEST(suite, test_add);
- return suite;
- }
如果有许多测试, 则要用到测试组的管理. 也就是测试 case 的管理, CuTest 中叫做 suite.
- CuSuite* CuGetSuite(void)
- {
- CuSuite* suite = CuSuiteNew();
- SUITE_ADD_TEST(suite, TestCuStringAppendFormat);
- SUITE_ADD_TEST(suite, TestCuStrCopy);
- SUITE_ADD_TEST(suite, TestFail);
- SUITE_ADD_TEST(suite, TestAssertStrEquals);
- SUITE_ADD_TEST(suite, TestAssertStrEquals_NULL);
- return suite;
- }
一般而言 suite 是一类测试的集合, 其实就是调用了 CuSuiteAdd 函数.
#define SUITE_ADD_TEST(SUITE,TEST) CuSuiteAdd(SUITE, CuTestNew(#TEST, TEST))
用宏展开,#TEST 等价于 TEST 内容转换为字符串, CuTestNew(#TEST, TEST)是宏的一种妙用. 此函数作用是把 case 加入到 testSuite 的具体链表中去.
- void CuSuiteAdd(CuSuite* testSuite, CuTest *testCase)
- {
- assert(testSuite->count <MAX_TEST_CASES);
- testSuite->list[testSuite->count] = testCase;
- testSuite->count++;
- }
上面是一类测试, 用 suite 函数 SUITE_ADD_TEST 来实现多个测试函数的归类管理. 那么有多个的函数的测试时候, 是如何规划呢, 需要 suite 上再添加 suite 了. 最后对上层接口提供一个总的 suite 的引用即可.
- CuSuite* suite = CuSuiteNew();
- CuSuiteAddSuite(suite, CuGetSuite());
- CuSuiteAddSuite(suite, CuStringGetSuite());
- CuSuiteAddSuite(suite, TestAdd());
2.3 测试的运行
测试 case 构成了测试组 --suite, 然后多个测试组可以合并为一个测试组. 测试组的执行就是遍历数组, 执行内部的每一个测试 case.
- void CuSuiteRun(CuSuite* testSuite)
- {
- int i;
- for (i = 0 ; i <testSuite->count ; ++i)
- {
- CuTest* testCase = testSuite->list[i];
- CuTestRun(testCase);
- if (testCase->failed) { testSuite->failCount += 1; }
- }
- }
测试的执行靠 CuTestRun 来完成, 依旧是打下跳转断点 --setjmp(buf), 然后运行测试 case, 如果测试 case 无错误, 则安静的退出, 否则记录出错信息, 然后 longjmp 返回到 if (setjmp(buf) == 0)一行, 在 CuSuiteRun 中, 会对错误 case 的个数进行计数, 以便全部 case 运行完毕后, 输出总结信息用.
- void CuTestRun(CuTest* tc)
- {
- jmp_buf buf;
- tc->jumpBuf = &buf;
- if (setjmp(buf) == 0)
- {
- tc->ran = 1;
- (tc->function)(tc);
- }
- tc->jumpBuf = 0;
- }
上面的函数, 测试函数的调用很隐晦, 是 (tc->function)(tc) 语句完成的. 测试 case 的原型为:
- typedef void (*TestFunction)(CuTest *);
- struct CuTest
- {
- char* name;
- TestFunction function;
- int failed;
- int ran;
- const char* message;
- jmp_buf *jumpBuf;
- };
所以 function 就指向具体的测试 case.
具体的实现为: 第一步创建测试 case, 即 CuTest* tc.CuTestNew 传入的参数 function 就是具体测试 case 函数的引用指针.
- CuTest* CuTestNew(const char* name, TestFunction function)
- {
- CuTest* tc = CU_ALLOC(CuTest);
- CuTestInit(tc, name, function);
- return tc;
- }
第二步, 测试 case 初始化, 将 funciton 引用指针赋值给 CuTest* t->function. 所以 (tc->function)(tc) 语句就相当于直接调用测试 case 函数本体.
- void CuTestInit(CuTest* t, const char* name, TestFunction function)
- {
- t->name = CuStrCopy(name);
- t->failed = 0;
- t->ran = 0;
- t->message = NULL;
- t->function = function;
- t->jumpBuf = NULL;
- }
3,CuTest 实例
下面是一个简单的实例, 包含了测试 case, 测试组, 测试执行.
1)测试 case
- void test_add(CuTest* tc)
- {
- CuAssert(tc, "\r\ntest not pass", 2 == 1 + 1);
- }
2)测试组 suite
- CuSuite* TestAdd(void)
- {
- CuSuite* suite = CuSuiteNew();
- SUITE_ADD_TEST(suite, test_add);
- return suite;
- }
3)测试项目结构组织
- void main()
- {
- RunAllTests();
- getchar();
- }
- void RunAllTests(void)
- {
- CuString *output = CuStringNew();
- CuSuite* suite = CuSuiteNew();
- CuSuiteAddSuite(suite, TestAdd());
- CuSuiteRun(suite);
- CuSuiteSummary(suite, output);
- CuSuiteDetails(suite, output);
- printf("%s\n", output->buffer);
- }
来源: https://www.cnblogs.com/pingwen/p/9216004.html