在写代码之前测试

作者: 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 */

终于通过了。

相关资料

极限编译
最近更新: $Date: 2006-03-05 11:10:30 +0800 (Sun, 05 Mar 2006) $