我们谈了不少测试的名词, 规范和原则 (link1link2). 软件是人写的, 测试计划和测试用例也是人写的, 人总会犯错误。错误发生之后, 总有人问: 为什么这个bug 没有测出来啊?!   我们看看一类简单的bug是如何发生的,以及如何预防它们再度发生: 

闰年

软件少不了和日期打交道, 日历系统算是人类的一个 legacy system, 这个系统在逐步进化的过程中, 打了好多补丁, 闰年就是补丁之一, 现在的spec 是: 4 年一闰, 100 年不闰,400年又闰。

 

错误之一: 算不清那一年是不是闰年。 1900 年是闰年么?

电子表格软件Excel 就有这样一个BugExcel 的日期计算功能认为1900年是一个闰年,这是不对的,但是它愣是一直没有改正这个错误。为什么屡教不改呢?

事是这样的,1980 年代, 这类电子表格软件的市场领头羊是Lotus 1-2-3一款软件。

 

现代软件工程讲义 9 测试 关于闰年的测试_system

来源: http://en.wikipedia.org/wiki/Lotus_1-2-3 

 

Lotus 1-2-3 占据了大部分市场份额, 不过, 它的日期计算功能有一个小Bug,就是把1900 年当作闰年。这类软件在内部把日期保存为“从1900/1/1 到当前日期的天数”这样的一个整数。Excel 作为后来者,要支持 Lotus 1-2-3 的数据文件格式,这样才能正确处理别的软件产生的格式文件。  这个错误就这么延续下来了,每一版本都有人报告,但是都没有改正。我们可以在Excel 中试试看:

在任意格子(cell)中输入“=DATE(1900,2,28)”,并且定义这个格子的格式为数字。大家可以看到数值变为:59表明1900/2/281900/1/1开始的第59天。

输入“=DATE(1900,2,29)”,可以看到 60! 这是一个不存在的日期!

输入“=DATE(1900,3,1)”,数值是61,事实上,这应该是60。从这一天开始的所有日期都错了一天。

 

改这个问题,技术上一点问题都没有。但是在现实中会出现下列问题:

1)几乎所有现存文件的日期数据都要减少一天,所有依赖于日期的 Excel公式也要做检查和修改。可以想象在计算利率,判断日期是否相等这些问题上都会出现细小而不能忽视的问题。 这在现实生活会造成很大的麻烦。

2Excel的日期问题解决了,但是其他软件还是有这个Bug,数据文件在不同软件中使用,就会有很头痛的兼容性问题。

 

下面是C# 的代码片段, 这段程序对么?

public static bool IsLeapYear(int year)
{
    System.Diagnostics.Debug.Assert(year >= 1900); 
    if (year % 400 == 0)
        return true;
    if (year % 100 == 0)
        return false;
    if (year % 4 == 0)
        return true;
    return false; 
}

错误之二: 计算错误

一个应用程序从另一个模块中接到一个数值, 是当天距离 [1980/1/1] 的天数, 现在要求这个程序返回今天的年份。 下面的程序怎么样? 有bug 么?

public static int NumberToYear(int days)
{
    int year = 1980; /* start with 1980 */
    System.Diagnostics.Debug.Assert(days >= 0);
    while (days > 365)
    {
        if (IsLeapYear(year))
        {
            if (days > 366)
            {
                days -= 366;
                year ++;
            }
        }
        else
        {
            days -= 365;
            year ++;
        }
    }
    return year; 
}

如果你要写这个程序的单元测试, 你会列出多少个测试用例? 你们保证所有代码路径都被覆盖么?

 

要写测试用例, 一个暴力的做法是穷举所有例子, 但是这有问题:

    1. 你穷举不完

    2. 即使穷举了很多例子, 但是它们未必能帮助发现 独特 的问题.  例如你可以测试输入 为 100, 101, 102, 103, 104, … 如果这个程序能正确处理 100, 它似乎也能处理101… 这些数。

我们要引入 “等价类 (Equivalence)” 这一概念。 一个粗浅的做法是:

如果一个函数可以返回 true | false, 你至少得有两类测试集合, 让它分别返回 true | false

如果你知道这个函数工作的原理, 或者了解程序要反映的现实世界, 你可以举出更详细的等价类, 例如针对 IsLeapYear():

被 400 整除的年份

被 100 整除, 但是不被400 整除的年份

被 100 整除, 同时被400 整除的年份

被 4 整除, 但是不被100 整除的年份

被 4 整除, 同时被100 整除的年份

偶数, 不被4 整除的年份

奇数年份

其它非法输入的年份

程序员都知道程序经常在边界条件附近出错, 针对IsLeapYear(), 你可以得出下面两个测试用例:

设计允许的最小的年份

设计允许的最大的年份

啊, 设计中没有考虑这个?   那这个设计要出现问题。  在1950-70 年代, 很多程序用两位数字表示年份 (00 – 99), 那些聪明的程序员认为这已经足够了, 没想到这些程序和设计影响了很多要和它们兼容的程序 (就像 Excel 要兼容 Lotus 1-2-3 那样), 到了1990年代后期, IT 业花了很多人力物力来解决 Y2K 的千年虫问题。 一些程序员非常钟爱的 UNIX 操作系统 (32 位) 也有自己的千年虫问题, 它会发生在 2038 年! 到时候人们还会用32位的机器么? 也许在一个大家想不到的关键部位, 一些老旧的, 嵌入式的 Unix 系统会悄悄地发作…