《單元測試實踐的主要問題及解決》由會員分享,可在線閱讀,更多相關《單元測試實踐的主要問題及解決(40頁珍藏版)》請在裝配圖網(wǎng)上搜索。
1、單元測試實踐的主要問題與解決
廣州凱樂軟件技術有限公司技術總監(jiān) 王彤
本文是我在“第十屆中國系統(tǒng)與軟件過程改進年會廣東會場”所作演講的整理稿,主要分享單元測試的一些要點、單元測試實踐的主要問題,以及如何來解決這些問題。
一、 單元測試概述
1.1 什么是單元測試
單元測試,就是針對代碼單元的獨立測試。為什么需要單元測試呢?這是代碼的基本特性決定了的。代碼有一個基本特性,就是對數(shù)據(jù)分類處理。
代碼通常會有很多的判定。一個判定,就是一次分類。嵌套的判定,會使分類次數(shù)的翻倍。
如果我們在寫代碼的時候,有一個分類漏掉了,就會產生一個Bug;如果一個分類,雖然寫了代碼,但
2、是處理不正確,也會產生一個Bug。一個函數(shù)要沒有錯誤,必須做到兩點:1,對數(shù)據(jù)的分類必須完整;2,每一個分類的處理必須正確。做到了這兩點,就可以說,代碼的功能邏輯是正確的。
那么,如何檢測代碼的功能邏輯是否正確呢?
調試,是臨時的,且不完整的,例如,一個函數(shù)有十種輸入,調試能覆蓋五六種就不錯了。而系統(tǒng)測試,并不針對某個具體的函數(shù),不關注某個函數(shù)的功能邏輯是否正確。
要檢測某個函數(shù)的功能邏輯,就必須要依照分類列出數(shù)據(jù),檢測代碼是否對每一個分類都做了處理,而且每一個分類的處理是否正確。
——這就是單元測試。
1.2 單元測試的基本方法
由上面的分析可以看出,單元測試的基本方
3、法就是:依數(shù)據(jù)的分類列出輸入,執(zhí)行被測試程序,然后,判斷輸出是否符合預期。
單元測試能達到什么樣的效果呢?那就是:無論別人怎么樣,我總是對的!
這里的“別人”,是指關聯(lián)代碼?!拔摇?,是指當前正在編寫或測試的代碼。單元測試要做到的是,無論關聯(lián)代碼是否有錯,都要保證我是對的。具體來說,我要考慮關聯(lián)代碼會產生什么樣的數(shù)據(jù),這些數(shù)據(jù)要如何分類處理,只要我的分類和處理是正確的,那么,無論別人怎么樣,我總是對的。
1.3 單元測試的效益
單元測試的效益可以說是立竿見影,并且會推動整個開發(fā)過程的改進。
首先,單元測試可以保證代碼的質量。因為只有單元測試,能夠全面檢測代碼單元的功能邏
4、輯,排除代碼中大量的、細小的錯誤。
其次,排錯成本最小。如果在編碼階段同時進行單元測試,排錯成本可以忽略不計。但若到了后期,排錯成本可能會增長上百倍,要是產品已經到了用戶手里,那造成的損失就更難說了。
第三,提升開發(fā)效率。單元測試可以讓程序行為一目了然,也就是程序行為可視化。什么叫程序行為呢?就是什么輸入下,會執(zhí)行哪些代碼,會產生什么輸出。如下圖,黑色的代碼是當前輸入下所執(zhí)行代碼。
如果我們寫幾行代碼,就可以看到程序的行為,相當于寫文章時上下文可見,這可以促進我們的開發(fā)思維。如果我們的思維有了偏差,也可以及時發(fā)現(xiàn)。如果代碼中有了錯誤,也可以隨時排除。
那么,是不是整個項目的
5、所有代碼都做了單元測試,才能得到這些效益呢?不是的。80:20規(guī)則,在軟件開發(fā)過程中也存在。也就是說,80%的代碼錯誤,可能存在于20%的代碼中;80%的編碼、調試成本,可能會消耗在20%的代碼上。這20%,就是算法密集度高的代碼,也就是功能邏輯復雜的代碼。
這些代碼可能只有20%,但是卻可能包含了80%的錯誤,消耗了80%的編碼、調試時間,即使只對這部分代碼進行單元測試,在提升產品的質量和開發(fā)效率方面,也會產生立竿見影的效果。
第四,自動回歸。如果沒有單元測試,系統(tǒng)測試發(fā)現(xiàn)了錯誤,當然要修改代碼,而修改代碼可能引入新的錯誤,又要進行全面的系統(tǒng)測試,這樣就可能陷入循環(huán),這通常也是項目延
6、期的主要原因。
如果有了單元測試,修改代碼時可以通過回歸測試馬上檢測是否引入了新的錯誤。所謂回歸,就是回復到原來正確的狀態(tài)。
正是回歸測試,使單元測試對整個開發(fā)過程的改進都產生積極影響,使項目適應頻繁變化的需求。單元測試是敏捷開發(fā)的基礎和核心,反過來說,有了單元測試,開發(fā)過程會自動趨于敏捷。單元測試也降低了后期測試的壓力。
二、 單元測試實踐的主要問題
單元測試有個特點:測試簡單獨立的代碼很容易,但要在實際工作中做好單元測試卻很困難。
根據(jù)我們的經驗,企業(yè)在實施單元測試時,通常會面對四大問題——
l 不愿做:程序員沒有單元測試習慣。
l 沒時間:編寫測試代碼需要耗費大量的
7、時間,項目的周期可能不允許。
l 做不了:代碼具有較高的耦合性,使單元測試難以進行。
l 做不好:測試效果不能令人滿意。我們通常會以覆蓋率來衡量測試效果,但要實現(xiàn)高標準的測試覆蓋很困難。
三、 解決思路和方法
如何解決上述問題呢?接下來,談談一些思路和方法,使用的工具是Visual Unit。Visual Unit,簡稱VU,是可視化的C/C++單元測試工具。
3.1 如何解決“不愿做”和“沒時間”
對于“不愿做”,我們采用的對策是可視化,這個可視化,是指程序行為可視,后面我會用案例來演示;對于“沒時間”,采用的對策是自動化,通過自動生成測試代碼、自動打樁等功能,讓測試的時
8、間成本最小化。這兩者結合起來,就是ETDD開發(fā)模式。
那么,ETDD是什么呢?
首先來介紹一下TDD,TDD就是測試驅動開發(fā),這個大家可能聽得比較多了。ETDD就是Easy TDD,即:易行版的TDD。ETDD具有以下一些特點:
l 可視化,在開發(fā)過程中,程序行為可視。
l 自動化,除了測試數(shù)據(jù)需要人工設定外,其他基本上都自動完成。
l 現(xiàn)實化,不一定要測試所有代碼,在開始階段,可以只測試功能邏輯復雜的20%代碼。
下面,我用一個案例,講解一下ETDD的過程:
假如我要編寫一個函數(shù),它的功能是刪除字符串左邊的空格。
先寫好函數(shù)的框架,能通過編譯就行。在編寫代碼前,程序員必須要做
9、的一件事情,是想清楚代碼的功能。如果我們想的時候,順手把它記錄下來,就可以讓代碼的功能更清晰、更明確。
我們現(xiàn)在來記錄代碼的功能。這里的記錄,不是文字形式的寵統(tǒng)說明,而是數(shù)據(jù)形式的精確定義,也就是用輸入和輸出的方式來記錄。
首先,記錄最基本的功能,也就是最基本、最常見的輸入和輸出。輸入一個左邊有空格的字符串,輸出是刪除左邊空格后的字符串,返回值跟參數(shù)的輸出是一樣的。
然后,記錄詳細的功能。例如,左邊沒有空格的,全是空格的,還有空字符串。
把每種輸入的正確輸出也記錄一下。完成了這個工作后,代碼的功能就完全定義下來了。
現(xiàn)在,我們開始編寫代碼。我的編碼思路是這樣的:分為兩
10、步,第一步計算左邊的空格數(shù)量;第二步,將非空格的字符向左移動,覆蓋掉左邊的空格。
以下幾行代碼,計算左邊的空格,現(xiàn)在編譯一下。CTRL+F7。如果編譯通過,測試就會自動運行。
我們可以看到,輸入是什么,執(zhí)行了哪些代碼,產生了什么輸出。這里,黑色的是當前輸入下所執(zhí)行的代碼,未執(zhí)行的話會顯示為紅色。這里全是黑色,表示當前輸入下執(zhí)行了全部代碼。如果我們想看一下計算左邊空格的結果對不對,這是內部的數(shù)據(jù),要指定位置后才會打印出來。按ESC鍵回到開發(fā)環(huán)境。
用這種語法可以輸出內部數(shù)據(jù),適合于程序員開發(fā)過程中使用。復雜類型也可以用同樣的語法輸出。
另一種輸出內部數(shù)據(jù)的語法是,在左邊的
11、代碼窗口,在要輸出的位置點擊一下,右鍵菜單選擇“輸出內部數(shù)據(jù)”,這樣填一下就行了。這種方式不會修改產品代碼,適合于測試員使用。
再次執(zhí)行后,可以看到,左邊的空格的數(shù)量是4,這是對的,那我們可以繼續(xù)編寫。
新加的這幾行代碼完成字符串的移動。這樣,代碼基本上寫完了,結果對不對呢?CTRL+F7編譯一下。
結果是完全不對的。我們來分析一下,輸入是這個,全部代碼都是黑色,表示都執(zhí)行到了,跟我設想的一樣。問題在哪里呢?
看一下計算左邊空格的代碼,經過計算后,指針偏移了,所以后面的計算,使用的是不正確的指針。
我們把指針先保存一下,第二次計算前再恢復回來??纯唇Y果怎么樣。
12、現(xiàn)在,參數(shù)的輸出是正確的了。但是,返回值還是不對,返回值應該跟參數(shù)一樣。分析一下,經過這里的計算后,指針再次偏移了,返回前沒有恢復,所以,返回的是不正確的指針。
返回前,再次把指針恢復??纯唇Y果。
現(xiàn)在,結果是正確的了。看一下測試結果,還有一個異常。
點擊它,可以看到,是空指針產生了這個異常,我們的代碼沒有對空指針進行處理。在這里,可以很清晰的看到代碼的執(zhí)行狀況。前面三行是黑色的,第四行開始都是紅色的,表示代碼只執(zhí)行到第三行,也就是說,第三行產生了異常。
添加處理空指針的代碼。
現(xiàn)在,代碼寫完了,單元測試也同步完成了。
我們來回顧一下ETDD過程:跟傳統(tǒng)
13、開發(fā)模式相比,ETDD多付出的,是把以前僅在頭腦里想的代碼功能記錄下來,從而精確地、完整地進行代碼的功能設計。
ETDD所得到的,是在編寫代碼的過程中,隨時可以看到代碼的行為,這可以讓我們的編碼過程變得輕松,而且也基本上不用調試,大家知道,調試,是最花費時間的。
另一方面,只要這里設定的數(shù)據(jù)是完整的,那么,我們的代碼就沒有問題。將來,如果需要修改代碼,只要重新執(zhí)行一下測試,就可以知道是不是破壞了原有的功能。
小結:ETDD通過可視化來幫助程序員輕松地編寫程序,單元測試不再是一個負擔;ETDD通過自動化,使程序員只需要在考慮代碼功能時順手記錄一下,其他工作都由工具完成。ET
14、DD提升了編碼的效率,也省略大部分調試,從而大幅提升了生產力。
3.2 如何解決“做不了”
上面我們只是用一個獨立的函數(shù)來演示ETDD過程。在實際的工作中,代碼之間通常是互相依賴的,這種依賴關系會造成測試難于進行,這就是“做不了”的問題。
我們首先來分析一下?!白霾涣恕敝饕侵缚蓽y性問題??蓽y性問題的核心是內部輸入。在解釋內部輸入前,我們先來看一下一般的輸入:外部輸入。
外部輸入是指在被測代碼的外部可以設定的輸入,包括參數(shù)、成員變量、全局變量。外部輸入一般可以直接設定。
單元測試的核心難點在于內部輸入,什么是內部輸入呢?
像下面這個例子,這兩個數(shù)據(jù),都是在被測試代碼
15、的內部,通過調用關聯(lián)代碼來取得,也就是內部取得的數(shù)據(jù)。對于內部取得的數(shù)據(jù),代碼要如何處理呢?跟參數(shù)一樣,也是分類處理。因此,測試時也要分類檢測,這就是內部輸入。
內部輸入有六種情形,我們利用工具都可以處理。
解決內部輸入的主要方法有打樁、模擬對象、底層模擬。
先來介紹打樁。樁就是代替真實代碼的一些代碼。樁的功能主要有隔離、補齊和控制??梢酝ㄟ^編寫樁代碼,來解決內部輸入問題。這是樁的控制功能。
用打樁來解決內部輸入,有一些問題:一是編寫樁代碼增加了工作量;二是內部輸入和外部輸入分離,難于管理;三是只能解決部分內部輸入問題。例如,要在一個用例中多次調用同一關聯(lián)函數(shù),要求每次輸出
16、不同,樁代碼就很難做到。
解決內部輸入的另一個方法是模擬對象,這個比較復雜,另外,對于C和C++也不太適用。我們可以采用底層模擬來解決內部輸入問題。
底層模擬有三個特點:一是內部輸入與外部輸入一起管理;二是不需要考慮關聯(lián)代碼的狀態(tài),無所關聯(lián)代碼是否存在,是否隔離,都可以直接使用;三是不需要編寫代碼。
下面我也用一個案例來講解一下底層模擬。這個示例,是一個空調控制程序。
代碼的功能,是首先取得環(huán)境的溫度,然后與預設的目標溫度比較,計算出溫度差,溫度每差一度,制冷器運行60秒。
首先,我們設定外部數(shù)據(jù)。假設,預設的目標溫度是25度,是這個全局變量,設為25。返回值為1,表示操作成
17、功。假設環(huán)境溫度是28度,那么,制冷器應該運行180秒,這里填180。然后執(zhí)行測試。
由于環(huán)境溫度還沒有設定,測試進行不下去。環(huán)境溫度由這個函數(shù)來取得。即使這個函數(shù)可以正常工作,取到的環(huán)境溫度也不可能滿足我們的測試需求。我們可以用底層模擬來解決。
首先,我們要讓這個取溫度的函數(shù)返回1,表示取溫度成功。雙擊函數(shù)名。
模擬值填1。
然后,設定環(huán)境的溫度。雙擊這個表示環(huán)境溫度的參數(shù)。
模擬值填28。
再看測試結果?,F(xiàn)在測試就可以正常進行了。這個參數(shù)的輸出是180,跟我們預期的一樣。內部輸入這里,顯示了兩個內部輸入。
這是我們設定的內部輸入,和外部輸入可以一起
18、管理。我們也可以把它移到表格中。
在表格中,我們增加一個用例,把溫度設為30,直接設定就是了。
這是環(huán)境溫度為30度時的測試結果,制冷器的運行時間為300。
上面演示的是簡單類型的底層模擬,復雜類型也一樣可以模擬,下面我演示一下。
這個底層函數(shù)返回的是一個對象指針,如何模擬呢?雙擊函數(shù)名,打開底層模擬器。
首先,在前置代碼中定義對象并初始化。然后,在模擬值中填寫這個對象的地址。
這是模擬的結果。
復雜對象的數(shù)據(jù)一樣可以移到表格中,這時,要移到表格中的不是對象本身,而是對象中包含的數(shù)據(jù)。例如,要把data.ui移到表格中,雙擊它的值“1234”就行了。
19、
我們還可以用局部數(shù)據(jù)模擬的功能,處理各種各樣的復雜情形。
例如,以下函數(shù)處理的是由界面輸入的數(shù)據(jù),這也是單元測試的一個難點??梢允褂镁肿償?shù)據(jù)模擬,把界面輸入轉換成普通的內部輸入。
這個函數(shù)的邏輯功能是計算SQL字符串,但計算結果沒有輸出到外部,這是內部輸出,工具也可以判斷內部輸出是否正確。
下圖是測試結果:
內部輸入解決之后,無論別人(關聯(lián)代碼),是否存在,是否正確,是否被隔離,都可以完整檢測我(當前代碼)。檢測我是否對所有數(shù)據(jù),包括內部輸入,都做了正確的分類和處理。
從而實現(xiàn)單元測試的目標:無論別人怎么樣,我總是對的! 如果所有代碼單元都做到了這一點,那會怎么
20、樣呢?整個項目就沒有代碼錯誤。
來看看嵌入式測試。在設備上進行單元測試不僅難度大、成本高,也無法達到應有的效果。如果在設備上測試,設備的一些輸出是難于控制的,例如這個例子,假設只有在發(fā)生雷擊時,獲取前車距離的函數(shù)才會返回失敗,那我們是不是等著雷擊呢?
即使不考慮成本,嵌入式單元測試也應該在PC上進行,這樣才能做到“我總是對的”。
3.3 如何解決“做不好”
現(xiàn)在來看做不好的問題。做不好的主要原因,是高標準的測試覆蓋難以實現(xiàn)。
為什么要關注測試覆蓋呢?因為未覆蓋的單位,通常對應未測試的數(shù)據(jù)分類,也就是說,可以用覆蓋率來檢查測試的完整性,衡量測試效果。
應該在完成功能測試的
21、基礎上,統(tǒng)計覆蓋率,找出遺漏用例來完成白盒覆蓋,而不是功能測試做一遍,白盒覆蓋又做一遍。
下面,我用一個案例來演示講解覆蓋。
首先是覆蓋率統(tǒng)計,工具可以支持六種覆蓋:語句、條件、分支、C/DC(判定條件覆蓋)、路徑覆蓋、MC/DC(修正判定條件覆蓋)。
哪些單位沒覆蓋呢?這個紅色且?guī)Уt色背景的,是未覆蓋語句;這個T是未覆蓋的條件真值;這個F是未覆蓋的條件假值;這個M是未覆蓋的MC/DC。
淡紅色背景的分支是未覆蓋分支,淡綠色背景的是已覆蓋分支。路徑是從入口到出口的路線,這條用綠色畫出的是已覆蓋的路徑。
這條用紅色畫出的是未覆蓋路徑。
如何完成覆蓋呢?點擊未覆蓋的單
22、位,比如這個T,右鍵菜單選擇“用例設計”。
工具會自動計算出一個近似用例,所謂近似用例,就是經過最小修改就可以覆蓋選中單位的用例。
如何修改呢?工具提供了修改提示,按這個藍色粗體的提示修改就可以了。這里的提示是A >1,把它改為大于1的數(shù),如2。在實際工作中,輸出也要根據(jù)功能進行修改,這里忽略。
執(zhí)行測試后,可以看到剛才那個T已經覆蓋了。點擊F,打開用例設計器。
這里的提示是B不等于0,把B改為不等于0,比如1。
現(xiàn)在來覆蓋這個T。
把X改為大于1的數(shù),如2。
現(xiàn)在,代碼這邊已經完成全部覆蓋了,看一下覆蓋率,還有一條路徑未覆蓋。
在這里選擇未覆蓋的路徑,打開用列設計器。
提示是:A不等于2,X小于等于1,X本來就小于1,不用改它,把A改為不等于2的數(shù)就行了,如3。
現(xiàn)在,完成了全部覆蓋。
總結
我們用可視化來解決“不愿做”,用自動化來解決“沒時間”,這兩者結合起來,就是ETDD開發(fā)模式。
造成做不了的主要原因是代碼的耦合關系形成的內部輸入問題,我們用底層模擬來解決內部輸入,真正可以做到“無論別人怎么樣,我總是對的”。
在覆蓋方面,我們利用工具不僅統(tǒng)計覆蓋率,清晰標示未覆蓋單位,而且,用例設計器可以幫助我們快速找出遺漏用例,實現(xiàn)高覆蓋,解決做不好的問題。
40 / 40文檔可自由編輯打印