曾经给某项目的重要底层模块做单元测试,打算写个系列文章来总结下。

测试对象:某项目重要底层模块代码(C 语言,约 6000+ 行代码)

测试方法: 白盒测试,先后进行了静态测试和动态测试。
静态测试主要是 code review,这也是我做单测之前耗时最多的准备工作,边研究代码边做静态测试(事实证明,此项目 80% 的 bug 都是在此阶段发现的)。
动态测试,使用了开源的 gtest 框架编写 test case,针对 6000 多行的被测代码,写了 1 万多行测试代码,被测代码与测试代码量大约为 1:1.8 的比例,被测函数与 test case 比约为 1:9。

测试时发现了一些比较经典的问题,主要有以下:

接下来的几篇文章会挑出几个比较典型的 bug 来展开说明。本篇就先以内存泄漏问题开始。
(说明:为了保护项目代码隐私,文章中均不会贴出被测代码,尽量以图文的形式来描述问题。)

内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。

测试时发现的内存泄漏 bug 主要有两个场景,但实际会非常多,这个会在后面进行说明。

内存泄漏 Bug1:

Bug 描述:函数内申请了堆内存,中途抛出异常直接 return 了,导致写在函数末尾的内存释放语句未执行。

解决:
各异常分支,在 return 前添加内存释放语句。

内存泄漏 Bug2:

Bug 描述:对于一个复杂的结构,内存分多次申请,在第 N 次申请内存失败或者遇到异常情况,直接返回,未将前 N-1 次申请的内存释放。
如下图:

结构体有两个成员,一个是 int 型名为 count,另外一个是指向 char* 的指针,这个指针指向一个 char* 的数组,数组总共有 count 个元素,每个元素的值代表了一个 char 字符串的存放地址。这个结构体和数组都是存放在栈上的,然后会有一个循环依次申请一块相应大小的堆内存,并将分配好的堆内存地址赋给相应的数组元素,一直循环 count 次,这个复杂数据结构的内存才算分配完成。代码中在每次分配堆内存之前会进行一些预置操作,在这个操作过程中如何出现异常,就会停止内存分配并退出。这里的问题就出现在退出之前没有将前面 N-1 次循环时申请的内存给释放掉,所以这里会产生内存泄漏。

解决:
在需要的所有堆内存空间被完全分配成功之前,发生任何异常,调用预先写好的 free_String_vector(nodes, j, 0) 函数将前面申请的 J 块内存释放掉。

总结:
其实上面两个 bug 出现的根本原因就在于在某些场景下内存释放语句被跳过去了。稍微分析下会发现代码中出现这种情况的场景会非常多,我罗列了一些供参考:
比较容易出现内存泄漏的情况:

Tips:不止堆内存的清理,类似的场景可以扩展到任意资源的清理,比如数据库连接、文件描述符、互斥锁、socket 套接字等。


↙↙↙阅读原文可查看相关链接,并与作者交流