多亏了回归测试。就在本周早些时候,我对自己编写并(自以为)调试过的新代码还颇为满意。我当时正在改进 OpenVOS 的 POSIX 函数,这些函数负责将二进制时间值转换为分解后的时间结构(C 语言专家们所说的“struct tm”)。 早在1998年,当我最初修改这些例程以适应POSIX环境时,为了图个捷径,直接调用了底层的VOS内核子程序。这种方法虽然快速简单,但意味着我们的POSIX运行时无法处理1970年至1980年之间的日期,因为VOS本身不支持这些日期。 最近,我一直在将几个主要的新开源软件包移植到 OpenVOS 17.0 上。我发现我们未能通过许多测试,原因正是无法处理这一时间范围。因此,我着手修改代码,以支持 VOS 和 UNIX 纪元(1970 年至 2048 年)内的所有日期。 我知道这项任务容易出错,但我决心要找到一种方法,彻底清除代码中的所有 bug。处理日期的一大优点在于其范围是有限的。此外,由于现代计算机速度相当快,让它们遍历所有可能的组合并观察结果并不难。 出于某些在此不便详述的原因,我特别想确保测试代码中处理1970年和1971年的那部分。于是,我编写了一个测试程序,从1970年1月1日开始,每天进行两次日期转换,一直持续到当前时间。该程序仅消耗了不到一秒的CPU时间。 果不其然,我在代码中发现了一个边界条件错误。修正错误后,测试通过了。我对这个过程感到相当满意,审核人员也批准了这些更改,我以为大功告成。随后,一位同事针对我的修改运行了回归测试套件。我们每次修改编译器及其运行时后都会进行此操作,以确保不会意外破坏任何功能。 不幸的是,几项回归测试失败了。当我查明为何测试未能发现此问题时——尽管测试看似十分彻底——我发现这些问题直到2038年才会显现。 虽然我在时间处理例程中添加了1970至1980年的十年区间,却删除了2038至2048年的十年区间。果不其然,当我回过头来测试该区间内的每一天时,证实了只要当初我要求测试,我的测试本可以发现这个问题。
这里有什么启示呢?我想最显而易见的启示是:如果你能测试所有组合,那就务必确保确实测试了所有组合。也许更重要的启示是,要花时间构建回归测试套件。这值得投入时间和精力。随着你对代码进行增强,回归测试套件可以降低发现和修复问题的成本,并缩短相关时间。 要证明构建回归测试套件的成本是合理的,其实不必避免太多用户报告的问题。我估计,随着开发流程中每个额外阶段的增加,修复软件问题的成本会增加一个数量级。由于任何开发流程中至少包含四个阶段(设计、编码、测试、部署),成本会迅速攀升。
