作者: Dai Yuwen
这是我几年前尝试单元测试时经历的事情。和大多数人一样,我写代码 的时候,信心十足,认为自己不会犯低级错误。但此次实践的结果让我震惊,至 今记忆犹新。我认识到:编程其实是一种“有罪推定”— 你写的代码在未经测 试前都是错误的。即使通过了某些测试,也只能说明在这些测试点上是正确的。其它 没测试到的代码,如果和人打赌的话,我认为出错的可能性远大于不出错的可能性。
事实证明单元测试的确有效,关 键是要有决心用相当于写代码的时间和精力来写这些测试代码。写代码的风 格也要发生变化,函数要写成没有“副作用”、只返回一个有用的值。 不过, 一旦完成所有重要函数的单元测试,每运行一次单元测试,你就“赚回”不少 时间—运行单元测试的人力成本几乎为零。只要被测函数的接口和返回类型不变,你可以任意改写此函数,因为有单元测试为你保驾!
测试尚未写出的函数my_malloc的代码是这样的:
gchar * test_my_malloc (cUnit_contextp data) { int *p = NULL; p = my_malloc (sizeof (int)); cUnit_assert (p); cUnit_assert (*p == 0); free (p); return NULL; }由于我使用了某种单元测试框架,此处出现了以cUnit_开始的函数或类型,您不 用深究, 只要了解cUnit_assert类似于<assert.h>里定义的断言就行 了。 这些测试代码可以看作是my_malloc的规范:我们期望它返回一个int型 指针、不 为空、指针所指的内容已经被清零。
而my_malloc函数与标准C库函数malloc一样,除了它要检查malloc的返回值:
void * my_malloc (size_t size) { void *p; p = malloc (size); if (!p) { perror ("my_malloc:"); exit (-1); } return p; }
但是测试没有通过。出错信息是:
cUnit: failed assertion cUnit_assert(*p == 0)
嗯?malloc没有把内存初始化为零? 我查了malloc的手册,果然malloc并无 此义务。于是我加了一行清零的代码:
void * my_malloc (size_t size) { void *p; p = malloc (size); if (!p) { perror ("my_malloc:"); exit (-1); } memset (p, 0, sizeof (size)); /* initialize */ return p; }
这下测试通过了。 由于test_my_malloc还不够通用—只测试了整型指针。我 把它改成了这样:
char * test_my_malloc (cUnit_contextp data) { char *p; int i; p = (char *)my_malloc (5); cUnit_assert (p); for (i = 0; i < 5; i++) { cUnit_assert (*(p + i) == 0); } free (p); return NULL; }
单元测试又没通过!
cUnit: failed assertion cUnit_assert(*(p + i) == 0)
怎么回事? memset没有把那块内存清零? 我又检查了my_malloc一遍。愚蠢的 错误:
memset (p, 0, sizeof (size)); /* initialize */ ^^^^^^
把sizeof去掉之后,测试通过了。
另一个例子:
test_UnitTest_ptrpath2strpath (cUnit_contextp data) { LINK *n1, *n2, *n3, *n4, *n5; char *str1, *str2, *str3, *str4, *str5, *result, *correct; str1 = ""; n1 = link_new_node (str1); result = UnitTest_ptrpath2strpath (n1); cUnit_assert (result == NULL); str1 = "abc"; n1 = link_new_node (str1); result = UnitTest_ptrpath2strpath (n1); str2 = "129"; n2 = link_new_node (str2); str3 = "8#^00012#$"; n3 = link_new_node (str3); cUnit_assert (strcmp ((char*)n3->item, "8#^00012#$") == 0); str4 = "vnls299s8"; n4 = link_new_node (str4); str5 = "oxld2"; n5 = link_new_node (str5); link_add_tail (n1, n2); link_add_tail (n1, n3); link_add_tail (n1, n4); link_add_tail (n1, n5); result = UnitTest_ptrpath2strpath (n1); correct = "abc.129.8#^00012#$.vnls299s8.oxld2"; cUnit_assert (result != NULL); cUnit_assert (strcmp (result, correct) == 0); return NULL; }
它要测试UnitTest_ptrpath2strpath函数。此函数使用了一个链表。链表 的节点的数据项是一个字符串。函数的作用是把所有这些字符串连成一个长串。 比如链表有3个节点。 节点1含有"this ", 节点2含有"is ", 节点3含有"a test"。UnitTest_ptrpath2strpath应该返回"this .is .a test"。
写完UnitTest_ptrpath2strpath之后,我运行了单元测试。 又没通过。这次 我使用GDB调试此函数,打印返回值:
p result "abc.129.8#^01012#$.vnls299s8.oxld2"
如果不仔细的话,可能还看不出此值与我的期望值correct有什么不同。让我 们检查UnitTest_ptrpath2strpath的代码:
... while (p) { substring = (char *)p->item ; len = strlen (substring); ^^^^^^^^^^^^^^^^^^^^^^^^ if (!len) { p = p->next; continue; } if (p->next) { string = realloc (string, len + 1); ^^^^^^^ ^^^^^^^ strcat (string, substring); strcat (string, "."); ...
我又犯了相同的错误,传了不正确的值给realloc。在
if (p->next) {之前加上一行:
len += strlen (string) + 1; /* total length */
终于通过了。