作者: 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 */
终于通过了。