专栏文章 白盒测试项目实战总结

360Qtest团队 · 2019年02月13日 · 2201 次阅读

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

测试对象:某项目重要底层模块代码(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 出现的根本原因就在于在某些场景下内存释放语句被跳过去了。稍微分析下会发现代码中出现这种情况的场景会非常多,我罗列了一些供参考:
比较容易出现内存泄漏的情况:

  • 本函数内申请内存,并在本函数内释放。由于一些原因导致内存释放语句被跳过或未来得及执行。(这种检查起来相对容易,甚至可以通过 code reivew 的方法就能发现)
    • 函数内抛出异常中断函数执行,在此之前未释放申请的内存;
    • 函数内有多个分支 return,即函数内部有多重回传路径,代码中是否在每次 return 之前都释放了内存;
    • 申请、释放内存的语句位于循环体中,某些情况下 continue、break,导致可能直接跳过了释放内存语句;
    • 以上情况下都相应释放了内存,但后期进行代码扩展,新增加了 return 路径或者重写了循环,忘记释放;
  • 函数内申请内存,由调用处释放或者穿越多层调用后释放,并且调用函数内部可能也有各种抛出异常,return,continue,break 等。这种情况仅通过 code review 明显是行不通的,这时就需要借助工具 + 动态测试结合进行。
  • 对于一个复杂的结构,需要分多次申请内存,在未完全分配完成之前,申请内存失败或遇到其他异常情况,应该释放掉前面申请的部分内存。
  • C++:析构函数中(本次测试的项目是 C 语言的,后来接触的 C++ 的项目,大多使用了智能指针来规避内存泄漏的问题)
    • 涉及到指针指向动态内存并且存在继承:虚析构函数
    • 显式定义赋值操作符,在赋值前,先释放掉之前指向的内存

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

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册