現代軟件工程講義 9 測試 關于閏年的測試
我們談了不少測試的名詞, 規范和原則 (link1, link2). 軟件是人寫的, 測試計劃和測試用例也是人寫的, 人總會犯錯誤。錯誤發生之后, 總有人問: 為什么這個bug 沒有測出來啊?! 我們看看一類簡單的bug是如何發生的,以及如何預防它們再度發生:
閏年
軟件少不了和日期打交道, 日歷系統算是人類的一個 legacy system, 這個系統在逐步進化的過程中, 打了好多補丁, 閏年就是補丁之一, 現在的spec 是: 4 年一閏, 100 年不閏,400年又閏。
錯誤之一: 算不清那一年是不是閏年。 1900 年是閏年么?
電子表格軟件Excel 就有這樣一個Bug:Excel 的日期計算功能認為1900年是一個閏年,這是不對的,但是它愣是一直沒有改正這個錯誤。為什么屢教不改呢?
故事是這樣的,1980 年代, 這類電子表格軟件的市場領頭羊是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/28 是1900/1/1開始的第59天。
輸入“=DATE(1900,2,29)”,可以看到 60! 這是一個不存在的日期!
輸入“=DATE(1900,3,1)”,數值是61,事實上,這應該是60。從這一天開始的所有日期都錯了一天。
改這個問題,技術上一點問題都沒有。但是在現實中會出現下列問題:
(1)幾乎所有現存文件的日期數據都要減少一天,所有依賴于日期的 Excel公式也要做檢查和修改。可以想象在計算利率,判斷日期是否相等這些問題上都會出現細小而不能忽視的問題。 這在現實生活會造成很大的麻煩。
(2)Excel的日期問題解決了,但是其他軟件還是有這個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) while (days > 365) |
如果你要寫這個程序的單元測試, 你會列出多少個測試用例? 你們保證所有代碼路徑都被覆蓋么?
要寫測試用例, 一個暴力的做法是窮舉所有例子, 但是這有問題:
- 你窮舉不完
- 即使窮舉了很多例子, 但是它們未必能幫助發現 獨特 的問題. 例如你可以測試輸入 為 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 系統會悄悄地發作…
除了從外部的輸入/輸出來設計測試用例, 我們也可以從內部考慮, 看看這些測試用例是否把所有語句都覆蓋了。 但是要注意, 即使所有語句都被測試用例覆蓋了, 程序還是可能出錯!
例如, 我們測試 NumberToYear(), 分析它的各個條件, 我們推算出我們的數據要覆蓋下面一些情況:
輸入的 day 大于 365
輸入的 day 小于 365
輸入的 day 大于 366 并且1980 年到那一年中, 至少有一年是閏年, 例如輸入一個2008年的某一天。
輸入的 day 大于 366 并且1980 年到那一年中不包括閏年。
這樣是不是就把所有路徑都包括了? 程序就沒有錯了?
不巧的是, 這個程序用在了某著名公司的產品上, 出品的前兩年沒什么事, 到了2008 年的最后一天 (那一年有366 天), 出了一個問題:
年份一直增加到了 2008, 這時候, day == 366, 我們看看循環能做下去么?
if (IsLeapYear(year))
{
if (days > 366) //day == 366, 不滿足條件
{
days -= 366;
year ++;
}
所以 day 沒有減少, year 也沒有增加, 循環又繼續下去, 任何條件都沒有改變, 進入了死循環!
不幸的是, 這個程序經過了種種測試, 進入了市場. 于是, 在2008 年的最后一天, 許多用戶發現他們的 Zune Player (只限于 Zune 30 型號) 開機之后就進入死鎖狀態…
http://www.usatoday.com/tech/products/2008-12-31-zune-mystery_N.htm
官方的說法是 - 大家等到明天就好了!
不用說這對于用戶, 對于產品的口碑, 對于這個代碼的開發者, 測試者是一個極大的打擊!
如果你是一個測試人員, 你應該增加什么測試用例呢? 如果用邊界條件分析, 應該有至少 4 個新的測試用例:
閏年的第一天;
閏年的最后一天;
平年的第一天;
平年的最后一天
對于程序員, TA 應該如何修改代碼?
錯誤之三: 沒想到還有閏年
在IT 行業混了很多年的好處之一就是你可以看到不少 bug. 下面又來了一個:
Windows Home Server與客戶端connector第一次連接時,需要Server為connector頒發安全證書。出于某種實現上無法避免的原因,客戶端的證書日期一定要早于Windows Home Server,否則生成證書的函數會fail。Windows Home Server是2007年7月RTM的。為了方便起見,設計中規定,給客戶端生成證書的函數使用2006年作為年份。
<完整的故事 link: http://yishan.cc/blogs/lilei105/archive/2008/03/03/885.aspx>
作為一個程序員, 你如何實現這個設計呢? 一拍腦袋, 就取當天的日期, 然后把日期中的年字段改成 2006, 不就行了么?
然后到了 2008/2/29 這一天… 程序把日期改成了 2006/2/29. 然后就悲劇了.
軟件團隊在自問: 為啥我們當初沒測出來? 如果你是測試人員, 你會想到這個測試用例么?
錯誤之四: 閏年bug 一天損失 30 萬
上面的錯誤都是外國軟件公司搞的, 我們看看中國的軟件 (還是嵌入式的軟件) 也不甘落后, 也創出了自己的閏年bug:
http://news.sohu.com/20120301/n336384709.shtml
-------------------------------
參考閱讀:
測試用例的等價類劃分和邊界條件分析:


浙公網安備 33010602011771號