單元測試是最早階段的軟件測試,面對的目標(biāo)最小,可以綜合使用黑盒測試方法和白盒測試方法,按理說,單元測試用例的設(shè)計(jì)應(yīng)該是最簡單的,但實(shí)際上,單元測試用例的設(shè)計(jì)常讓人感覺無從下手,這是什么原因?是代碼真的不具有“可測性”嗎?還是測試思路和方法不對?正確的測試思路和方法是什么?單元測試工具應(yīng)該具備什么樣的功能,才能支持快速地構(gòu)建測試用例?
大道至簡,意思是掌握了事物的本質(zhì),事情就會變得很簡單。反之,如果事情很復(fù)雜很麻煩,往往表示沒有抓住本質(zhì)。
單元測試的本質(zhì)是什么?首先要看單元測試的目標(biāo)是什么。單元測試檢測代碼功能邏輯,實(shí)現(xiàn)高質(zhì)高效的編程。只要真正做過單元測試的工程師都知道,單元測試要做的,能做的,就是檢測代碼的功能邏輯,扯上其他東西是沒有任何意義的。既然單元測試是對代碼功能邏輯的檢測,那么,測試用例要做的,就是針對代碼的功能邏輯,設(shè)定其輸入,并判斷其輸出是否符合預(yù)期,從而檢測功能邏輯的正確性。功能邏輯是由什么實(shí)現(xiàn)的?邏輯塊。所以,單元測試的本質(zhì),是面向邏輯塊,單元測試用例的本質(zhì),是邏輯塊的輸入輸出,也就是設(shè)定邏輯塊的各種可能輸入,及對應(yīng)的預(yù)期輸出。
一旦我們把目光轉(zhuǎn)向邏輯塊,所有的事情就會變得簡單。來看一個(gè)典型示例。“典型”的意思是,如果這個(gè)代碼不會測,實(shí)際項(xiàng)目也就不會測,因?yàn)橥瑯拥臏y試問題會大量存在;反過來,如果這個(gè)代碼可以測得很好很快,那么實(shí)際項(xiàng)目的測試就基本上沒有問題,因?yàn)橐呀?jīng)掌握了正確的測試思路和測試方法。這個(gè)代碼的功能是,取得職位列表,將職位標(biāo)題拼成短信并發(fā)送給用戶。參數(shù)是一個(gè)數(shù)據(jù)流,包含用戶的手機(jī)號和想要什么類型的職位等信息,程序從數(shù)據(jù)庫里讀取對應(yīng)的職位列表和一個(gè)映射表,映射表用來檢查哪些職位已經(jīng)發(fā)送給用戶,然后把職位的標(biāo)題拼成短信并且發(fā)送給用戶。代碼使用C++編寫,但它所表達(dá)的測試問題和測試思想,則是通用的。
請想一想,這個(gè)代碼的測試思路是什么?也就是說,哪些變量要設(shè)置輸入,哪些變量要判斷輸出?如果按照傳統(tǒng)的方法,輸入是參數(shù),輸出是返回值,那基本沒法測。但是如果面向邏輯塊(上面的代碼分為兩張圖片,第二張圖片實(shí)現(xiàn)函數(shù)的功能邏輯,也就是我們要測的邏輯塊),立刻就有了思路:輸入是鏈表對象objList和映射表對象map里的數(shù)據(jù),輸出是拼接出來的字符串,在兩個(gè)地方需要判斷它的值。
具備了面向邏輯塊的測試思路,就可以將“可測性”這個(gè)詞扔進(jìn)垃圾桶了,除非代碼真的糟糕得太過分,否則都不難測。至于代碼之間的耦合,那是再正常不過的事情,代碼反映了客觀事物,客觀事物本身就是互相關(guān)聯(lián)的,代碼能沒有耦合?如果一個(gè)函數(shù)有多個(gè)邏輯塊,同樣很簡單,各個(gè)邏輯塊分別測試就是了。
面向邏輯塊,測試用例的數(shù)據(jù)也很簡單,因?yàn)檫壿嬘?jì)算涉及到的數(shù)據(jù)往往很少,且一般是基本類型。上面的示例,數(shù)據(jù)算是比較麻煩的,多數(shù)代碼的測試數(shù)據(jù)量都少于這個(gè)示例。雖然這個(gè)示例的數(shù)據(jù)看起來很嚇人,又有鏈表,又有映射表,但實(shí)際上,邏輯計(jì)算中涉及到的輸入就是一些標(biāo)題(title),字符串而已,也就是說,我們要加入到鏈表和映射表中的對象指針,不管它的類型多復(fù)雜,每個(gè)對象只需要設(shè)定標(biāo)題(title)就OK了。至于輸出,也不過是字符串。總之,對于這個(gè)示例,用例的輸入是一系列字符串,輸出也是一些字符串。
傳統(tǒng)單元測試思想,是面向函數(shù),即用例由函數(shù)的輸入輸出構(gòu)成,這在一些特例中沒有問題,例如三角形函數(shù)、排序函數(shù)之類最底層函數(shù)。這些函數(shù),只有一個(gè)邏輯塊,且邏輯塊的輸入輸出,與函數(shù)的輸入輸出完全一致,當(dāng)然可以使用函數(shù)的輸入輸出來構(gòu)建測試用例。傳統(tǒng)的單元測試用例設(shè)計(jì)技術(shù),都拿這些特例作為基礎(chǔ),當(dāng)面對含有耦合關(guān)系的代碼時(shí),反而作為特例來處理,要使用編寫樁代碼、設(shè)置模擬對象之類的麻煩方法來解決(實(shí)際上很多時(shí)候解決不了問題,例如,前面示例中的輸出怎么辦?)。實(shí)際項(xiàng)目中,代碼存在耦合關(guān)系是常態(tài),完全沒有耦合的代碼反而很少。這種拿特例當(dāng)常態(tài),拿常態(tài)當(dāng)特例的方法,在本質(zhì)上是錯(cuò)誤的,因此必然很麻煩,從根本上造成了單元測試難度大、成本高??傊?,面向函數(shù)來設(shè)計(jì)單元測試用例,測試將很困難,至于主張單元測試要面向?qū)ο?、面向模塊,那純粹是胡扯。
有了面向邏輯塊的測試思想,測試思路是很簡單了,但是,如何設(shè)置邏輯塊的輸入值和輸出值呢?邏輯塊的輸入,除了參數(shù)、成員變量之類的常規(guī)變量,還包括底層輸入,即調(diào)用底層函數(shù)獲得的輸入,如前面示例中的鏈表和映射表對象中的數(shù)據(jù);很多時(shí)候,還包括局部輸入,即在被測試代碼執(zhí)行過程中對某些變量的實(shí)時(shí)賦值,如局部靜態(tài)輸入、中斷輸入、界面輸入等。邏輯塊的輸出,除了返回值、成員變量之類的常規(guī)變量,還包括局部輸出,即被測試代碼執(zhí)行過程中對某些變量的實(shí)時(shí)判斷,如前面示例中直接發(fā)送出去的短信需要在發(fā)送前實(shí)時(shí)判斷。這些問題,恰恰表明了單元測試的另一個(gè)簡單:選擇工具很簡單。如果工具不能直接地、方便地設(shè)定邏輯塊的輸入輸出,那基本上沒法用,或者成本很高(至少十倍以上),因此,選擇工具的最主要指標(biāo),就是能否直接地、方便地設(shè)定邏輯塊的輸入輸出。C/C++單元測試工具Visual Unit 4可以通過在表格中填寫數(shù)據(jù),直接設(shè)定邏輯塊的輸入輸出,例如前面的示例,使用Visual Unit 4,只要點(diǎn)點(diǎn)鼠標(biāo),在表格中填寫一些字符串,就可以構(gòu)建出鏈表和映射表中的數(shù)據(jù),以及判斷所拼接的短信是否正確。
也許有人認(rèn)為,對于前面的示例,如果面向函數(shù),通過設(shè)定參數(shù)來獲得鏈表和映射表的數(shù)據(jù),也可以達(dá)到同樣的測試效果,甚至可以同時(shí)檢測代碼所調(diào)用的其他函數(shù),例如用于解析用戶信息的GetUserInfo()函數(shù),用于從數(shù)據(jù)庫讀取職位列表的GetJobList()函數(shù)。這種想法是完全錯(cuò)誤的,白白浪費(fèi)時(shí)間和精力,為什么?
1、這些函數(shù)可能還沒有實(shí)現(xiàn),這在并行開發(fā)中很常見;
2、這些函數(shù)或者它們所依賴的函數(shù)在測試時(shí)可能被隔離,這在大型項(xiàng)目中很常見;
3、相關(guān)設(shè)備在測試時(shí)可能不存在,例如,單元測試一般不連接數(shù)據(jù)庫;
4、相關(guān)設(shè)備無法返回測試需要的數(shù)據(jù),例如,一個(gè)取環(huán)境溫度的底層函數(shù),總是返回固定值;
5、即使以上問題都不存在,通過設(shè)置參數(shù)來間接獲得邏輯塊的輸入也可能非常困難,例如前面的示例,必須熟悉通訊協(xié)議,了解GetUserInfo()函數(shù)的工作過程,并在參數(shù)中填寫正確的數(shù)據(jù)流,且數(shù)據(jù)庫里有合適的數(shù)據(jù),才可能獲得鏈表和映射表中的數(shù)據(jù)。
面向邏輯塊,則完全不需要考慮這些問題,無論多大的項(xiàng)目,無論多少人并行開發(fā),都可以在開始編寫代碼時(shí),就做到邊開發(fā)邊測試。至于底層函數(shù),誰家的孩子誰抱,應(yīng)該由編寫者直接進(jìn)行測試,這樣才能全面地檢測它的功能邏輯。
總之,單元測試應(yīng)該面向邏輯塊,只有這樣,才能迅速產(chǎn)生測試思路,才能快速構(gòu)建用例數(shù)據(jù),才能檢測功能邏輯的方方面面,不留死角,而判斷一個(gè)單元測試工具是否可以高效地應(yīng)用于實(shí)際項(xiàng)目,最主要的指標(biāo)是能否直接地、方便地設(shè)置邏輯塊的輸入輸出。