讀古今文學網 > C語言解惑 > 13.8 軟件測試 >

13.8 軟件測試

正確性是程序最重要的屬性。即使是很小的程序,要想對它採用嚴格的數學證明方法來證明其正確性,也是非常困難的,因此只好求助於程序測試過程來實施這項工作。程序測試是指在目標計算機上利用輸入數據(也稱為測試數據)運行該程序,將其運行結果與所期望的結果進行比較。如果兩種結果不同,就可判定程序存在問題。然而不幸的是,即使兩種結果相同,也不能夠斷定程序就是正確的,因為對於其他的測試數據,可能會得到不同的結果。如果使用了許多組測試數據都能得到相同的結果,則可增加對程序正確性的信心。不過,要想通過使用所有可能的測試數據來驗證程序是否正確,對於大多數實際的程序,可能的測試數據的數量就不知有多大。顯然不可能進行窮盡測試,因此實際用來測試的輸入數據只能是整個輸入數據空間的子集,稱之為測試集。例如關於變量x的二次函數,其標準形式為


ax
2+bx+c=0
  

其中a,b,c的值是已知的,且a≠0。

【例13.14】下面的程序計算並輸出該二次方程的根。


#include <stdio.h>
#include <math.h>
void FindRoots
(double a
, double b
, double c
)
{  //
計算並輸出一個二次方程的根
       double d=b*b-4*a*c
;                    //1
       if
(d>0
) {                              //2  
兩個實數根
            double sqrtd=sqrt
(d
);
            printf
(\"
有如下兩個實數根:n\"
);
            printf
(\"%f
和%fn\"
,(-b+sqrtd
)/
(2*a
),(-b-sqrtd
)/
(2*a
));
       }                                   //6
       else if 
(d==0
)                         //7  
兩個根相同
           printf
(\"
只有一個實數根:%fn\"
,(-b/
(2*a
)));     //8
       else {                              //9  
複數根
           printf
(\"
有如下兩個複數根:n\"
);
           printf
(\"%f+%fin\"
,-b/
(2*a
),sqrt
(-d
)/
(2*a
));
           printf
(\"%f-%fin\"
,-b/
(2*a
),sqrt
(-d
)/
(2*a
));
       }                                   //13
}
  

程序運行的結果應是:當d=0時,所得到的兩個根是一樣的;當d>0時,兩個根不同且是實數;當d<0時,兩個根也不相同且為複數。

現在不去試圖對該函數的正確性進行形式化證明,而是希望通過測試來驗證其正確性。不難知道,對於該程序來說,所有可能的輸入數據的數目實際上就是所有不同的輸入數據(a,b,c)的數目,其中a≠0。即使a,b和c都被限制為整數,所有可能的測試數目也是非常巨大的,因此要想測試所有的輸入數據是不可能的。若整數的長度為16位,b和c有2 16種不同取值,a有2 16-1種不同取值(因為a不能為0)。所有不同測試組的數目將達到2 32*(2 16-1)。如果目標計算機能按每秒鐘1000 000個測試數據的速率進行測試,至少也需要9年才能完成!所以實際使用的測試集僅是整個測試數據空間的一個子集。

由於可以提供給一個程序的不同輸入數據的數目一般都非常巨大,所以測試通常都被限制在一個很小的子集中進行。使用子集所完成的測試不能完全保證程序的正確性,所以測試的目的不是去建立正確性認證,而是要暴露程序中的錯誤!必須選擇能暴露程序中所存在錯誤的測試數據,不同的測試數據可以暴露程序中不同的錯誤。

如果使用數據(a,b,c)=(1,-5,6)來進行測試,程序將輸出2和3。程序的行為與期望的行為是一致的,因此可以推斷對於該輸入數據,程序是正確的。然而,使用一個適當的測試數據子集來驗證所觀察行為與所期望行為的一致性,並不能證明對於所有的輸入數據程序都能夠正確工作。一段錯誤的代碼也可能給出正確的結果,例如:如果在關於表達式


d=b*b-4*a*c
  

中忽略a,將其錯誤地寫成


d=b*b-4*c
;
  

在使用數據(a,b,c)=(1,-5,6)來進行測試時,d的值及所測試的結果仍與原來正確的結果相同,這是因為a=1。但是實際上由於使用測試數據(1,-5,6)而未能執行完代碼中的所有語句,即第6條以後的語句沒執行,因此對這些語句正確性還沒有多大的把握。假設使用下面的測試數據:


void main
()
{
     FindRoots
(1
,-5
,6
);  
     FindRoots
(1
,3
,2
);
     FindRoots
(2
,5
,2
);
     FindRoots
(1
,-8
,16
);
     FindRoots
(1
,2
,5
);
}
  

測試程序輸出如下:


有如下兩個實數根:
3.000000
和2.000000
有如下兩個實數根:
-1.000000
和-2.000000
有如下兩個實數根:
-0.500000
和-2.000000
只有一個實數根:4.000000
有如下兩個複數根:
-1.000000+2.000000i
-1.000000-2.000000i
 

由此可見,因為測試集{(1,-5,6),(1,3,2),(2,5,2)}的每個測試數據僅需執行代碼的前6行語句,所以這個測試集僅可用來暴露FindRoots前6行語句中存在的錯誤。測試集{(1,-8,16),(1,2,5)}執行第2條和第7條的判斷語句,只有測試集{(1,2,5)}才執行全部判斷語句,所以該測試集將可以暴露較多的錯誤。

可以斷定,不可能對一個軟件進行徹底的測試。問題是要選擇合適的測試技術,通過執行有限個測試用例,盡可能多的發現軟件錯誤。

歸納起來,軟件測試的目標如下:

(1)測試是一個以找出錯誤為目的的執行軟件的處理過程。

(2)好的測試用例必須有很高的發現錯誤的概率。

(3)成功的測試是一種能暴露出尚未發現的錯誤的測試。

因此,決不能把一個成功的測試當作不發現錯誤的測試。恰恰相反,它應當是一種可以系統地暴露出各種不同類型錯誤的測試。只有如此,才能對軟件質量做出明確的保證。為實現這個目標,應對軟件實施一系列的測試步驟。每個測試步驟通過採用一系列系統的測試技術,有效地選擇測試用例來完成。除了人工測試技術以外,目前出現了越來越多的自動測試工具,以輔助進行軟件開發過程中最為困難、代價高昂的測試工作。

一般來講,可以把軟件測試分為模塊測試、組裝測試和確認測試。

13.8.1 模塊測試

因為模塊測試是實現階段最為重要的一個軟件工程步驟,是軟件質量保證的關鍵環節,即使經過代碼評審,模塊中必然要留存許多未被發現的邏輯錯誤,必須通過測試來暴露。這其實也是在程序組裝成一個整體之前,分別測試各個模塊的操作。其優點如下:

(1)可以全面測試各個函數。當完成了對某個函數的測試之後,就可以確信它能夠正確地工作。

(2)測試容易控制。若某個函數的測試失敗,可立即把問題定在該函數之內。對於通常的測試方法,最大的問題就是當發現程序不能正常工作時,又找不出程序的錯誤究竟在哪裡。

(3)測試數據容易構造。可以測試函數而不必把測試數據放入數據文件之中。測試數據的構造對通常的測試方法是一個很棘手的問題,其結果常常造成測試不充分。

在測試之前,必須理解究竟要證明什麼,測試什麼和怎樣測試。測試應該按源文件分組,因為在每個源文件中的函數不能分開,所以把它們放在一起測試。對每個被測試的源文件,都要列出測試程序清單,同時給出測試運行結果。

程序功能描述、偽碼程序和源文件清單,對於決定測試什麼和怎樣測試,將有很大的幫助。一般可以使用特殊的函數測試main()函數,即設計一個虛構的執行過程,替代實際的函數體。它們包括前面敘述過的printf()語句,告知它們何時被調用和顯示所接收的參數,但返回值將從鍵盤上獲得。這樣的特殊函數常常稱為樁(stub)函數。

按照軟件工程的觀點,模塊測試應以詳細設計描述為指導,按照測試計劃來進行。

模塊測試主要評價模塊的5個特性:

(1)模塊接口;

(2)模塊內部數據結構;

(3)重要的執行路徑;

(4)錯誤處理路徑;

(5)影響上述幾點的邊緣條件。

在開始其他任何測試之前,必須首先測試貫穿模塊入口和出口的數據流。如果數據不能正確地進入和退出模塊,其他各項測試便無從下手。接口測試內容包括:

(1)輸入參數的數目、次序和屬性是否與變元相一致;

(2)輸出的變元數目、次序和屬性是否與調用該模塊的參數相一致;

(3)調用的內部函數的參數的數目、次序和屬性是否正確;

(4)對參數的任何訪問是否與調用模塊無關;

(5)輸入是否只改變變元;

(6)全部變量定義是否相容。

在模塊實現外部I/O時,還需附加以下的接口測試:

(1)文件屬性是否正確;

(2)OPEN語句是否正確;

(3)I/O語句與格式說明是否匹配;

(4)記錄長度與緩存區的大小是否匹配;

(5)文件是否在處理之前打開,並在處理之後關閉;

(6)是否處理了文件結束條件;

(7)是否處理了輸入輸出錯誤。

對於模塊中的局部數據結構應當測試:

(1)不正確的或不相容的數據說明;

(2)置初值錯誤或錯誤的默認值;

(3)錯誤的變量名;

(4)不相容的數據類型;

(5)下溢、上溢或地址錯;

在模塊測試期間,對關鍵的軟件路徑的測試是一項重要工作。測試用例應當能測試出不正確的計算、錯誤的比較或者控制流向不正確而引起的各種錯誤。這些錯誤包括:

(1)算術優先級不正確或理解錯誤;

(2)運算方式混淆;

(3)置初值錯誤;

(4)精度不夠;

(5)表達式的操作符的使用錯誤;

(6)不同的數據類型比較;

(7)邏輯運算符或優先級不正確;

(8)循環異常終止或死循環;

(9)出口錯誤;

(10)循環變量修改不正確。

一個好的軟件必須自身能夠預見錯誤條件,並且當錯誤出現時,能通過錯誤處理路徑提示用戶改正錯誤並重新處理,或者有效地結束處理。因此,測試用例的設計應包括足夠的錯誤條件,以揭示在這方面可能出現的潛在錯誤。這包括:

(1)出錯說明是否可理解;

(2)指示的錯誤是否對應實際遇到的錯誤;

(3)錯誤條件是否在錯誤處理之前已經引起系統干預;

(4)提供的出錯說明是否足以幫助確定出錯位置。

邊緣測試是模塊測試的最後一個步驟。軟件經常在邊緣下出錯。例如,在循環處理一維數組的n個元素時,在循環的第一次和第n次往往發生錯誤。因此採用剛剛小於或恰好等於最大值,剛剛小於或恰好等於最小值的測試數據往往能揭示出數據結構、控制流和數值結果方面的許多錯誤。

如果時間因素是模塊的重要特徵,那麼還要專門進行關鍵路徑測試,以便確定最壞情況及平均意義下影響模塊執行時間的因素。

圖13-13給出測試與其他階段的關係,由此可見,錯誤應及早發現和解決,否則解決後期的錯誤將花費更大的代價。

圖13-13 測試與其他階段的關係

13.8.2 組裝測試

組裝測試是軟件生存週期中的一個獨立階段。其主要任務是按照選定的策略,採用系統化的方法,將經過模塊測試的模塊按預先制定的計劃逐步進行組裝和測試。這種測試的目的在於發現與模塊接口有關的問題,並將各個模塊構成一個設計所要求的軟件系統。

為了完成上述任務,組裝測試應按以下步驟進行。

(1)執行測試計劃中說明的所有系統組裝測試;

(2)改正測試中暴露出來的錯誤;

(3)分析測試結果;

(4)書寫測試分析報告;

(5)組織人員嚴格評審,直至通過為止。

另外,還應同時完成可運行的系統源程序清單和組裝測試分析報告。

13.8.3 確認測試

測試的最後一個步驟也是軟件開發的最後一個階段,是驗證所組合的軟件系統是否確實滿足用戶的需要。這是軟件開發部門把軟件產品交付使用之前的最後一項測試,稱為確認測試。因此,這個測試步驟所發現的錯誤往往是「軟件需求規範書」中的錯誤。