只有深刻理解內部輸入,才能真正理解單元測試。單元測試是針對代碼單元的獨立測試,一個函數,在調用了其他函數的情況下,如何能夠獨立測試?只有把底層函數的輸出,視作被測函數的一種輸入,才能真正進行獨立測試。
把底層函數的輸出視為被測函數的輸入,會不會影響測試效果?當然不會,因為單元測試主要目的就是檢查被測函數的功能邏輯,檢查是否針對各種輸入包括內部輸入做了合適的處理。無論底層函數是否正確或者是否存在,只要被測函數對輸入包括內部輸入的各種可能做了正確的處理,被測函數本身就不會有功能錯誤。
內部輸入是單元測試的關鍵難題。代碼耦合有兩種,虛耦合和實耦合。虛耦合是指沒有調用關系的耦合,例如我們要測試Compare(),這個函數位于一個文件A中,文件A中可能還有很多其他代碼,也可能包含了很多頭文件,雖然這些代碼并沒有被Compare()調用,但是,這些代碼仍然可能造成文件A難于單獨編譯和鏈接,這就是虛耦合,虛耦合一般可以靠簡單打樁解決。實耦合就是調用關系形成的耦合,例如在代碼一中,Comapre()和GetArea()就是實耦合,實耦合又有兩種情況:一是底層函數沒產生被測函數需要使用的輸出,這種情形可以不處置;二是底層函數產生了被測函數需要使用的輸出,成了內部輸入。
內部輸入有以下幾種情形:
自然內部輸入
這是指對底層函數的正常調用即可獲得的內部輸入,前面的示例就屬于自然輸入。代碼一中Compare()函數,int a1 = GetArea(r);可以自然取得外接正方形的面積。如果外接矩形面積a1要得到某個預期的值,要傳遞合適的半徑r。自然輸入有兩個條件:一是底層函數存在,二是底層函數正確。
不可控的內部輸入
是指調用實際代碼,但實際代碼的輸出難于控制,難于把各種可能輸出都測試到。例如,底層函數返回一個隨機數,就是不可控。在實際項目中,不可控是很常見的,下面的代碼是空調控制程序中的一個函數(代碼清單4.3.cpp):
/* 函數說明: 功能: 空調控制程序片斷,取得環境溫度并計算制冷器需運行的時間 參數: pWorkTime, 輸出參數,保存制冷器需運行的時間 返回: int類型,如果函數執行失敗,返回0,否則返回非0值 */ extern int GetTemperature(int* pTemperature); int gExpectTemperature = 25; int WorkTime(int* pWorkTime) ////取環境溫度 if(!success) //后面的代碼與_03_WorkTime2完全一致 //為了簡化問題,這里假設溫差一度,需運行一分鐘 |
代碼的重點在于success = GetTemperature(&temperature);,這行代碼調用GetTemperature()取環境溫度,如果操作成功,success等于1,操作不成功,success等于0;取得的環境溫度保存在局部變量int temperature中。假設在實際環境中測試,調用的都是實際代碼。我們首先要設定預期的溫度gExpectTemperature,例如設為25,這是全局變量,容易做到。我們還要測試各種環境溫度下程序的行為,例如,至少要測試25,大于25和小于25三種情況,顯然,這是很困難的,真實的環境溫度在短時間內很難大幅變化,即使大幅變化,也未必符合測試需求,這就是不可控。
失真的內部輸入
失真是打樁造成的,是打樁的必然后果。上面的示例,假如GetTemperature()未實現,或者由于解耦合的目的必須隔離,或者試圖解決不可控的問題打樁來代替,樁代碼大致是這個樣子(代碼清單4.4.cpp):
int GetTemperature(int* pTemperature) { return 0; } |
直接返回0,此外什么也不做。調用GetTemperature()后,success總是為0,環境溫度temperature未初始化,測試無法進行。
一種思路是修改樁代碼,使它實現一些功能,例如,給每個用例起一個名字,樁代碼判斷當前用例名并做合適的操作。這種方法比較麻煩,并且只能適應簡單情形。一個樁可能被多個被測函數調用,一個被測函數又可能調用多個樁,要維護用例名與樁行為之間的匹配關系,無疑是一場噩夢。
難于設定的內部輸入
前面介紹自然內部輸入時提到:如果圓的外接正方形a1要得到某個預期的值,要傳遞合適的半徑r,這是通過外部輸入來獲得預期的內部輸入,即需要倒推外部輸入。很多時候,這個工作是很困難的,例如,要設定圓的面積為10.00,半徑應該是多少?另外,很多時候,為了獲得一個簡單的內部輸入,需要做復雜的初始化工作,請看下面的示例(代碼清單4.5.cpp):
/* 函數說明: 功能: 將PERSON對象指針保存到表格中,如果名字已存在,則不保存并返回0 參數: pData, 需保存的對象指針 map, 保存對象指針的映射表 返回: 如果加入失敗,返回0,否則返回非0值 */ int AddPerson(PERSON* pData, CPersonMap2* map) { if(map->Search(&pData->name)) return 0; map->Add(pData); |
參數PERSON* pData是結構指針,記錄一個“人”的資料,結構PERSON含有一個字符串成員name,記錄“人”的名字。參數CPersonMap2* map是一個映射表,以名字為key保存PERSON的對象指針。代碼很簡單,當名字已在表中存在時,直接返回0,否則保存到映射表。測試時要使 map->Search(&pData->name)返回true,一般的方法是在表中預先加入相應的數據,這可能很麻煩,如果能直接讓map->Search(&pData->name)返回true,既簡單又直接。
難于設定的內部輸入非常常見,尤其是測試比較高層的函數,很多輸入都是比較復雜但其目的只是傳遞給底層函數以獲得一個簡單的內部輸入,如果我們轉換思路,想辦法直接設定內部輸入,工作將會大量減少。
靜態變量形成的內部輸入
除調用底層函數形成內部輸入外,局部靜態變量也會形成內部輸入。請看下面的代碼(代碼清單4.6.cpp):
/* 函數說明: 功能: 游戲程序中用于計算打擊效果的代碼片斷,連續打擊時效果隨次數遞減 參數: reset, 輸入參數,為true時重置打擊次數 返回: int類型,打擊效果 */ int PowerEffect(bool reset) { //打擊次數,由于是局部變量,用例中無法訪問,難于測試 static int times = 0; if(reset) times = 0; times++; int effect[] = {9, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; |
打擊次數times是一個局部靜態變量,局部變量無法外部訪問,這給測試造成了困難。這個示例中,打擊次數只是簡單遞加,還有可能通過適當排列用例,或插入若干前置調用來控制它的值,但在實際項目中,未必那么簡單,因此,局部靜態變量也是一種必須解決的內部輸入。
前面列出了內部輸入的五種情形,后四種是影響單元測試能否順利實施的關鍵難點。有問題并不可怕,可怕的是不知道問題的存在。只有發現和正視問題,才有可能解決問題。有趣的是,有些朋友將內部輸入的問題被歸結為“代碼可測性差”,解決辦法是改良代碼提高可測性,這是典型的“站著說話不腰疼”,有經驗的程序員用腳趾頭想一下,就知道這些問題是大量存在并且多數是不可能消除的。單元測試方法或工具,如果無法解決內部輸入的問題,就無法適應實際項目的測試,這不是代碼的可測性問題,而是方法或工具的可用性問題。
文章來源于領測軟件測試網 http://www.k11sc111.com/