一、“千般路”與“磨豆腐”
很久以前聽一個(gè)故事:從前有個(gè)小伙子,少時(shí)有大志,長大后卻無好營生,開了個(gè)豆腐作坊,每天磨豆腐累得腰酸背疼。每到夜深人靜,小伙子輾轉(zhuǎn)反側(cè),總想找條更好的“事業(yè)之路”,可是想過千百條、嘗試過幾十條路,都走不通。夜不成寢,白天干活更累,小伙子不由慨嘆:“晚上想過千般路,白天還得磨豆腐”。
不久以前看過一篇文章:《CMM欺騙了中國的軟件業(yè)》,內(nèi)容是對CMM熱的反思。CMM當(dāng)然不會(huì)主動(dòng)欺騙人,實(shí)際上是我們的軟件業(yè)自己欺騙自己。我們從來不缺少“某某模式”,“面向某某”,“某某認(rèn)證”等等聽起來美妙無比的東西,問題是實(shí)際的研發(fā)過程中能做得到碼?現(xiàn)實(shí)是殘酷的,美妙的概念漫天飛舞,開發(fā)過程仍然是作坊式的,正是:“晚上想過千般路,白天還得磨豆腐”。
中國的故事通常都有圓滿的結(jié)局,現(xiàn)在接著說“磨豆腐”的故事。過了很長時(shí)間,小伙子終于面對現(xiàn)實(shí),不再沉迷于不切實(shí)際的空想,用心磨好豆腐,閑時(shí)琢磨些個(gè)竅門,慢慢地,他的豆腐質(zhì)量越來越好,每天產(chǎn)量也越來越多,作坊越開越大,成了遠(yuǎn)近聞名的“豆腐老板”,后來,他做起了別的生意,發(fā)現(xiàn)年輕時(shí)的空想,其實(shí)很多都是可行的,因?yàn)楝F(xiàn)在“能力”和“財(cái)力”都不同了。
再說軟件開發(fā)。我們不反對任何理論、技術(shù)、方法、模式等等,但第一,您的企業(yè)或團(tuán)隊(duì)做得到嗎?不要做“如果開發(fā)時(shí)間延長一倍,就可以做到”之類毫無意義的假設(shè)。第二,做了真的有效益嗎?效益是指扣除成本之后的收益。如果不具備這兩點(diǎn),那么還是不要整天想著“千般路”,首先想想如何好好的“磨豆腐”吧。
對于所有軟件開發(fā)來說,代碼編寫都是無可逃避的“磨豆腐”。改進(jìn)代碼編寫工作,高率效低成本地開發(fā)出高質(zhì)量的代碼,對于軟件產(chǎn)品能否在激烈的競爭中勝出,對于軟件企業(yè)的生存和發(fā)展,都具有重要的現(xiàn)實(shí)意義。
本文是Visual Unit應(yīng)用的范例項(xiàng)目C++代碼文檔生成器的主題文檔,敘述的正是改進(jìn)代碼編寫工作的方法和工具,所有內(nèi)容均經(jīng)過實(shí)戰(zhàn)檢驗(yàn),具有"可行"和"效益"兩個(gè)特征,"可行"是指較低門檻或沒有門檻,憑現(xiàn)有條件即可實(shí)施;"效益"是指能產(chǎn)生立竿見影的效果。
本文所援引的范例項(xiàng)目,模擬最糟糕的開發(fā)團(tuán)隊(duì),最混亂的開發(fā)流程:由很少寫代碼的測試和預(yù)研部門開發(fā),人員不固定,時(shí)間也不固定,誰有空就寫上一些;沒有設(shè)計(jì),沒有文檔,基至也不在代碼文件中保存編碼人員的信息,成員完全依賴于閱讀代碼和測試用例來理解其他成員寫的代碼;除了簡單的命名規(guī)則外,沒有其他規(guī)范,甚至連一個(gè)函數(shù)原則上不能超過50行之類的基本規(guī)范也沒有(范例中有超過200行的函數(shù)CMacro::Unwind(),一萬多條路徑)。任何開發(fā)團(tuán)隊(duì)和開發(fā)流程都會(huì)好于范例項(xiàng)目的開發(fā)團(tuán)隊(duì)和開發(fā)流程,因此,范例所展示的方法和工具,具有"廣泛可行性"。
本文介紹如何進(jìn)行高效編碼調(diào)試和實(shí)現(xiàn)徹底的單元測試。編碼調(diào)試是任何軟件開發(fā)都無可逃避的工作,在Visual Unit的支持下編碼調(diào)試,只是把本來就一定要做的工作改變一下方式,不需要多做什么,就可以大幅提高編程效率和質(zhì)量;另一方面,Visual Unit徹底改變了單元測試難于實(shí)施或成本昂貴的局面,無論團(tuán)隊(duì)中開發(fā)與測試人員的比例是怎么樣的,都可以輕松快捷地實(shí)現(xiàn)徹底的單元測試。
二、高效編碼調(diào)試
任何軟件開發(fā),都離不開編碼調(diào)試。對于稍為復(fù)雜一點(diǎn)的函數(shù),一般來說,編寫幾行代碼,就要執(zhí)行一下,看它們是否按預(yù)想的工作,然后再繼續(xù)寫,寫完后還要將各種可能輸入都執(zhí)行一下。如何執(zhí)行?一般由別的代碼來調(diào)用,也就是說需要驅(qū)動(dòng),驅(qū)動(dòng)通常是在開始編寫函數(shù)實(shí)現(xiàn)代碼之前建立,這樣才能一邊編寫一邊調(diào)試。驅(qū)動(dòng)大致可分為自然驅(qū)動(dòng)和專門驅(qū)動(dòng)。
自然驅(qū)動(dòng):利用項(xiàng)目中已有的代碼作為驅(qū)動(dòng),通常是在被調(diào)試的函數(shù)中加斷點(diǎn),從界面執(zhí)行一個(gè)需要調(diào)用該函數(shù)的功能,調(diào)試器中斷時(shí)就可以調(diào)試了;專門驅(qū)動(dòng):為需要調(diào)試的函數(shù)編寫專門的驅(qū)動(dòng)代碼,通過執(zhí)行驅(qū)動(dòng)代碼來執(zhí)行被調(diào)試函數(shù)。
自然驅(qū)動(dòng)的主要優(yōu)點(diǎn)是不需要其他工作就可以直接調(diào)試,甚至感覺不到需要驅(qū)動(dòng),主要缺點(diǎn)是輸入數(shù)據(jù)通常是公共的,即很多代碼都使用相同的輸入源進(jìn)行調(diào)試,實(shí)際輸入往往是經(jīng)過其他代碼處理后的中間結(jié)果,要針對各種可能輸入都進(jìn)行調(diào)試往往很困難,造成調(diào)試不全面,程序員的思維受到局限,難于做到全面地考慮各種可能輸入。
專門驅(qū)動(dòng)的主要優(yōu)點(diǎn)是輸入數(shù)據(jù)是專門針對于被測試程序,容易做到比較全面,程序員的思維也會(huì)比較全面,對編寫功能齊全的健壯的程序很有好處,要針對某種特定輸入進(jìn)行調(diào)試比較容易,缺點(diǎn)是需要花費(fèi)大量的時(shí)間來編寫驅(qū)動(dòng)代碼。
顯然,自然驅(qū)動(dòng)的主要問題是不全面,代碼錯(cuò)誤較多,專門驅(qū)動(dòng)的主要問題是編寫驅(qū)動(dòng)代碼很費(fèi)時(shí)。有沒有更好的方法,既不需要編寫驅(qū)動(dòng)代碼,又能方便且全面地調(diào)試?有 !這就是自動(dòng)驅(qū)動(dòng),即在Visual Unit的支持下編碼調(diào)試,不但無需費(fèi)時(shí)間寫驅(qū)動(dòng)代碼,更擁有多種獨(dú)特的便利,可以大幅提高編碼調(diào)試的質(zhì)量和效率。
Visual Unit是單元測試工具,但也是高效編程調(diào)試的支持環(huán)境,在Visual Unit的支持下調(diào)試,既全面又省時(shí):
自動(dòng)生成驅(qū)動(dòng)代碼,但又可以方便地設(shè)定調(diào)試輸入;
測試用例編輯器列出全部輸入,可以很方便地檢查是否全面。
除了上述優(yōu)點(diǎn)外,在Visual Unit的支持下調(diào)試,還可以:
可視化地選擇調(diào)試輸入;
調(diào)試過程中還可以切換輸入;
無限制的后退,重復(fù)。
上述僅是免費(fèi)的個(gè)人版的功能,對于企業(yè)版用戶,實(shí)際上大多數(shù)單步調(diào)試都可以省略:
自動(dòng)輸出參數(shù)、成員變量的輸入輸出值,返回值,用戶也可以用簡單的語法輸出任何變量或表達(dá)式的值,這些數(shù)值都是上下文相關(guān)的;
顯示在一個(gè)用例下,程序所執(zhí)行的代碼,可以很方便地查看程序是否按預(yù)想的流程執(zhí)行。
程序無論多復(fù)雜,無非就是執(zhí)行一些代碼,讀寫、計(jì)算一些數(shù)據(jù),因此,上述兩方面信息已完整地描述了程序行為,一眼就能看出程序干了什么,通常可以很快判斷程序是否按預(yù)想的工作并找到出錯(cuò)原因,比單步調(diào)試要快得多。
下面以實(shí)例來進(jìn)一步分析三種調(diào)試方式的優(yōu)缺點(diǎn)。這里所用的示例是范例項(xiàng)目中的CExFunction::ParseOneParameter()函數(shù),這是一個(gè)很普通的函數(shù),讀者也可以隨便拿其他有些復(fù)雜度的代碼來比較。該函數(shù)的功能是解析C++代碼中的一個(gè)參數(shù),原形如下:
PARAMETER* CExFunction::ParseOneParameter(CTokenList& iList);
PARAMETER 是保存一個(gè)參數(shù)對象的結(jié)構(gòu),定義如下:
struct PARAMETER
{
CString type; //參數(shù)類型
CString name; //參數(shù)名
CString defVal; //缺省值
CString array; //如果參數(shù)是數(shù)組,保存[]及[]內(nèi)的文字常量
};
參數(shù)iList是一個(gè)輸入?yún)?shù)(范例的命名規(guī)則是用i表示輸入?yún)?shù)),傳遞由C++代碼中的一個(gè)參數(shù)經(jīng)過詞法分析轉(zhuǎn)換獲得的記號(hào)對象序列,例如參數(shù)int* pi,將轉(zhuǎn)換為三個(gè)記號(hào)對象,分別對應(yīng)于:int, *, pi。該函數(shù)將記號(hào)對象序列解析到一個(gè)PARAMETER結(jié)構(gòu)的指針中,并作為返回值返回。
在這個(gè)示例中,如果要進(jìn)行比較全面的調(diào)試,輸入至少要考慮以下可能:
普通輸入,如int i;
類型中有符號(hào),如int* pi;
類型中有多個(gè)符號(hào),如int*& pi;
模板類,如CList list;
帶缺省值,如int i=0;
數(shù)組,如int ai[10];
類型有多個(gè)單詞,如const unsigned int& i;
缺少參數(shù)名,如const int;
我們在編寫這個(gè)函數(shù)的實(shí)現(xiàn)代碼前,首先建立調(diào)試驅(qū)動(dòng),以例邊編碼邊執(zhí)行調(diào)試。
自然驅(qū)動(dòng)
假設(shè)界面和要調(diào)用這個(gè)函數(shù)的其他代碼都已完成。在函數(shù)的入口處插入斷點(diǎn),以調(diào)試方式運(yùn)行工程,在界面中選擇要生成文檔的工程目錄,點(diǎn)擊"生成文檔",程序中斷時(shí)就可以調(diào)試了。這種方式相信所有程序員都很熟悉,并且很多人都會(huì)認(rèn)為這種方式最省時(shí),但實(shí)際上,這種方式只是開頭省時(shí),當(dāng)你試圖把各個(gè)可能輸入都調(diào)試一遍,就會(huì)發(fā)現(xiàn)它很費(fèi)時(shí):可能輸入通常分散分布于輸入源中(這里的輸入源是代碼文件),如果要比較全面地調(diào)試,通常要整理輸入源,否則幾乎不可能全面地調(diào)試,也就是說,要全面地調(diào)試,仍然要費(fèi)時(shí)間整理輸入,并不能完全依賴自然輸入;
要針對某種輸入進(jìn)行調(diào)試,例如要調(diào)試參數(shù)帶有缺省值的情形,一般通過反復(fù)跟蹤直到想要的輸入出現(xiàn),或者設(shè)置條件斷點(diǎn)攔截所需要輸入,反復(fù)跟蹤當(dāng)然費(fèi)時(shí)不少,設(shè)置條件斷點(diǎn)也是要花時(shí)間的,并且有時(shí)無法滿足需要 ,很多時(shí)候,要針對特殊輸入進(jìn)行調(diào)試都是很大費(fèi)時(shí)的;
由于是公共輸入源,輸入數(shù)據(jù)很難管理,尤其是條件斷點(diǎn)更不可能無限期地保存,以后需要再次調(diào)試時(shí)可能要做很多重復(fù)工作。
如上所述,自然驅(qū)動(dòng)并不省時(shí),不過這種方式的時(shí)間消耗隱藏在調(diào)試過程中,通常不會(huì)引起重視,其實(shí)"隱藏于調(diào)試過程中",其成本更大,因?yàn)榉稚⒘碎_發(fā)人員的注意力,影響思維的連續(xù)性。
自然驅(qū)動(dòng)的更主要問題是不全面,開發(fā)人員常常會(huì)將思維局限于現(xiàn)有的輸入源,導(dǎo)致一些可能輸入根本就沒有考慮到,在本例中,很可能只是試一下解析一兩個(gè)文件,檢查得到的結(jié)果是否正確,如果文件中沒有 帶數(shù)組的參數(shù),那這種輸入很可能就被忽略掉。這種不全面,導(dǎo)致代碼功能不齊,健壯性差,后期測試和維護(hù)成本居高不下,甚至導(dǎo)致項(xiàng)目的失敗,因此,這是看起來高效,實(shí)際上很低效的方式,讀者可以在看完后面兩種方式的介紹后,自 已嘗試并對比一下,可以拿任何有一定復(fù)雜度的代碼編寫來對比,不局限于這里所舉的例子。
專門驅(qū)動(dòng)
專門驅(qū)動(dòng)離單元測試只有一步之遙了,只要在驅(qū)動(dòng)代碼中添加判斷預(yù)期輸出的語句就構(gòu)成了完整的測試代碼,因此,在實(shí)際工作中,采用專門驅(qū)動(dòng)最好是邊編碼邊測試,并使用測試代碼作為調(diào)試驅(qū)動(dòng)。下面是為函數(shù)CExFunction::ParseOneParameter ()編寫的調(diào)試驅(qū)動(dòng)代碼:
{CExFunction* pObj = new CExFunction();
CTokenList iList;
CTokenReader reader;
reader.ReadTokenList(iList, "int i");
PARAMETER* ret = pObj->ParseOneParameter(iList) ;
ASSERT( ret->type == "int" );
ASSERT( ret->name == "i" );
reader.ClearTokenList(iList);
delete pParam;
delete pObj; }
上面的代碼其實(shí)是一個(gè)測試用例的完整代碼,測試代碼通常都是很簡單的,功能無非是使被測試的代碼得于執(zhí)行,被測試代碼通常都涉及到外部數(shù)據(jù),如參數(shù)、成員變量、全局變量什么的,這些數(shù)據(jù)當(dāng)然要設(shè)定初始值,例如,上面的測試用例是將字符串"int i"經(jīng)過CTokenReader對象的ReadTokenList方法轉(zhuǎn)換成CToken對象指針的列表作為參數(shù)iList的輸入。
在實(shí)際工作中,函數(shù)的輸入輸出常常不是簡單的數(shù)據(jù)類型,而是某些對象甚至是對象的集合,本例中,輸入的數(shù)據(jù)就是CToken對象指針的列表。這種情況下,一般借助現(xiàn)有的代碼來生成數(shù)據(jù),通常,這些"現(xiàn)在代碼"都是存在的,因?yàn)榧词共蛔鰷y試,也總有代碼要調(diào)用該函數(shù)的,調(diào)用代碼本來就需要生成相應(yīng)的數(shù)據(jù)。本例中,CTokenReader::ReadTokenList()函數(shù)就是把字符串轉(zhuǎn)換為CToken對象指針的列表。
只要寫完了第一個(gè)測試用例的代碼,更多的用例就簡單了,只要拷貝并對輸入輸出數(shù)據(jù)進(jìn)行修改就行。細(xì)心的讀都可能已注意到,第一個(gè)測試用例的前后加了{(lán)},這是為了多個(gè)測試用例可以使用相同的變量名。
使用這種方法,建立測試代碼通常是很快的。編寫很簡單的函數(shù)時(shí)不需要調(diào)試,當(dāng)然也不需要測試代碼。測試代碼的組織也很簡單:一個(gè)產(chǎn)品工程對應(yīng)一個(gè)測試工程,一個(gè)產(chǎn)品類對應(yīng)一個(gè)測試類,一個(gè)需要測試的產(chǎn)品函數(shù)對應(yīng)一個(gè)測試函數(shù)。測試工程可以加一個(gè)簡單的界面,以便執(zhí)行指定的測試,也可以使用相應(yīng)的工具如 CppUnit。
再回到我們的主題:調(diào)試。有了測試代碼,調(diào)試就簡單了,要調(diào)試某種輸入,只要在相應(yīng)的測試用例中加斷點(diǎn)就行了。使用這種方式,僅就調(diào)試來說,好處也是非常明顯的:
所有輸入在一起列出來,調(diào)試比較全面,程序員的思維也會(huì)比較全面;
要調(diào)試指定的輸入很容易,通常不需要高級(jí)斷點(diǎn),更不需要通過反復(fù)跟蹤來捕捉需要的輸入;
調(diào)試數(shù)據(jù)可以永久保存,避免了以后修改代碼時(shí)的重復(fù)工作。
自動(dòng)驅(qū)動(dòng)
在Visual Unit的支持下編碼調(diào)試,除了兼具自然驅(qū)動(dòng)和專門驅(qū)動(dòng)的優(yōu)點(diǎn)外,還能享受Visual Unit的獨(dú)特殊功能帶來的便利。
首先我們用個(gè)人版來說明,個(gè)人版是免費(fèi)的版本,并且開發(fā)商也提供免費(fèi)的基本技術(shù)支持。
Visual Unit具有豐富的文檔,包括視頻教程,這里不再敘述其基本使用方法。只要選擇要測試(調(diào)試)的類和函數(shù)(如果使用企業(yè)版的IDE插件,會(huì)根據(jù)當(dāng)前文檔和光標(biāo)位置自動(dòng)選擇),VU就會(huì)生成測試代碼,并彈出測試用例編譯器。VU是自動(dòng)生成測試代碼,而不是自動(dòng)生成測試用例,也就是說,輸入輸出數(shù)據(jù)是由用戶指定的,不過VU已經(jīng)生成了輸入輸出數(shù)據(jù)的聲明。下面是本例中VU生成的第一個(gè)測試用例的輸入輸出:
輸入部分:
CTokenList iList =
輸出部分:
ret ==
這里的“=”和“==”僅僅表示可能需要賦值或判斷輸出,對于基本數(shù)據(jù)類型,可以直接填寫數(shù)值,高級(jí)數(shù)據(jù)類型需要靈活處理(詳細(xì)信息請查看幫助或教程)。只要把輸入輸出改為這樣就完成了第一個(gè)測試用例的建立:
輸入部分:
CTokenList iList = //多余的=會(huì)自動(dòng)刪除
CTokenReader reader
reader.ReadTokenList(iList, "int i")
輸出部分:
ret->type == "int"
ret->name == "i"
更多的測試用例,只要點(diǎn)擊"新建",就會(huì)自動(dòng)拷貝當(dāng)前用例,只要修改輸入輸出就行了。這里沒有涉及到成員變量和變局全量,不過都很簡單的(成員變量用點(diǎn)操作符訪問,全局變量直接訪問),請查看幫助。
可以看出,使用VU建立調(diào)試支持環(huán)境是很快速的:對于第一個(gè)測試用例,輸入輸出比較復(fù)雜時(shí)需要寫少量簡單的代碼,輸入輸出簡單時(shí)直接填寫輸入輸出數(shù)值,其他測試用例只需點(diǎn)擊一個(gè)按鈕拷貝現(xiàn)有測試用例并修改輸入輸出就行,可以選擇相近的用例來生成新的用例,這樣通常只需要修改一兩個(gè)數(shù)據(jù)就可以得到想要的用例。
那么,還可以得到哪些好處呢?
方便地進(jìn)入調(diào)式:在被調(diào)試函數(shù)的入口加斷點(diǎn),并調(diào)試測試工程即可進(jìn)入調(diào)試;
方便地選擇輸入:在測試用例編輯器中輕點(diǎn)鼠標(biāo)即可指定要調(diào)試的輸入,如果執(zhí)行了測試,只要點(diǎn)擊出錯(cuò)的測試,就會(huì)自動(dòng)選擇相應(yīng)的輸入;
方便地切換輸入:調(diào)試過程中,不需要退出調(diào)試,就可以切換到其他輸入:只要在測試用例編輯器中選擇另一個(gè)測試用例,用調(diào)試器的"執(zhí)行到光標(biāo)所在行"命令回到函數(shù)入口,即可切換到新的輸入;
無限制后退重復(fù):用調(diào)試器的"執(zhí)行到光標(biāo)所在行"命令可以自由地后退和重復(fù)執(zhí)行,其實(shí)現(xiàn)的原理是"重來",后退時(shí)相關(guān)數(shù)據(jù)也會(huì)"還原",感覺上是真正的"后退",這個(gè)奇特而有用的功能是VU生成的測試代碼自動(dòng)實(shí)現(xiàn)的。
上述是免費(fèi)的個(gè)人版所具有功能,VU企業(yè)版除了具有這些功能外,還具有"描述程序行為"的功能:
自動(dòng)輸出參數(shù)、成員變量的輸入輸出值,返回值,用戶也可以用簡單的語法輸出任何變量或表達(dá)式的值。這些數(shù)值都是上下文相關(guān)的,也就是說,同一個(gè)用例的相關(guān)值放在一起;
顯示在任一個(gè)用例下程序所執(zhí)行的代碼,可以很方便地查看程序是否按預(yù)想的流程執(zhí)行。
程序無論多復(fù)雜,無非就是執(zhí)行一些代碼,讀寫、計(jì)算一些數(shù)據(jù),因此,上述兩方面信息已完整地描述了程序行為,很容易看出"程序干了什么"。這個(gè)功能大幅度地提高了開發(fā)人員的工作效率:
幫助整理、驗(yàn)證編程思路:寫幾行代碼,就可以看看"程序干了什么",輕松判斷"現(xiàn)在所寫的對不對",也比較容易想清楚"接下來要怎么寫"。快速找出程序錯(cuò)誤:根據(jù)輸入輸出數(shù)據(jù)和所執(zhí)行的代碼,通常可以很快判斷程序是否按預(yù)期工作并找到出錯(cuò)原因,比單步調(diào)試要快得多。編程的時(shí)間消耗主要不在于敲鍵盤,而在于編程思路和調(diào)試,VU企業(yè)版可謂"對癥下藥",在這兩方面大量提高工作效率。
以上所述,都是針對軟件開發(fā)過程中無可逃避的工作:編碼調(diào)試。僅從時(shí)間上來說,對于編寫調(diào)試有一定復(fù)雜度的程序,使用專門驅(qū)動(dòng),可能比自然驅(qū)動(dòng)多費(fèi)一點(diǎn)時(shí)間,但也是完全合算的,至于自動(dòng)驅(qū)動(dòng),如果使用VU個(gè)人版,大概能省時(shí)10%,如果使用企業(yè)版,則可以省時(shí)20-50%!讀者可以自行嘗試比較一下。是否省時(shí)還不是最重要的,更高的價(jià)值在于:使用專門驅(qū)動(dòng)或自動(dòng)驅(qū)動(dòng)編碼調(diào)試,實(shí)際上也已經(jīng)把令人望而生畏的單元測試工作完成了一半,并清除了實(shí)施單元測試的最主要障礙。
三、實(shí)現(xiàn)徹底單元測試
是什么使單元測試難于實(shí)施?首先是代碼的可測性。可測性是什么?如果一個(gè)類具有基本的可測性,那么把它加入到另一個(gè)工程后(當(dāng)然有關(guān)聯(lián)的文件也要加入)能夠通過編譯,這其實(shí)是很低的要求,但對于一個(gè)有一定規(guī)模的項(xiàng)目,如果開發(fā)調(diào)試時(shí)使用自然驅(qū)動(dòng),在完成編碼后才進(jìn)行單元測試,那么通常都不具有可測性,因?yàn)殚_發(fā)人員常常在無意之中使代碼之間產(chǎn)生了不當(dāng)耦合,這些不當(dāng)耦合累積起來,會(huì)使整個(gè)項(xiàng)目的代碼糾纏在一起,造成難于測試。
使單元測試難于實(shí)施的另一個(gè)方面是建立測試用例。在本例中,如果由不熟悉代碼的測試人員建立測試用例,那么他很可能不知道如何生成CToken對象 指針的列表。
如果邊開發(fā)邊使用專門驅(qū)動(dòng)(測試代碼放在另一個(gè)工程中)或自動(dòng)驅(qū)動(dòng)調(diào)試,那么一旦出現(xiàn)影響可測性的不當(dāng)耦合就會(huì)及時(shí)發(fā)現(xiàn)及時(shí)解決,保證了代碼的可測性,另一方面,由于至少建立了一個(gè)測試用例,測試人員建立其他用例時(shí)只要修改一下輸入輸出數(shù)據(jù),從而大大降低建立測試用例的難度。總之,使用專門驅(qū)動(dòng)或自動(dòng)驅(qū)動(dòng)調(diào)試,在不增加開發(fā)工作量的同時(shí),已經(jīng)為單元測試打一下了堅(jiān)實(shí)的基礎(chǔ)。
范例項(xiàng)目V0.1處于這樣一個(gè)階段:剛剛完成代碼編寫,并未完成單元測試,現(xiàn)有的多數(shù)測試用例都是編碼時(shí)用于調(diào)試的。在此基礎(chǔ)上,單元測試由誰做,難度都不大。VU的典型應(yīng)用是通過三個(gè) 階段來完成針對一個(gè)函數(shù)的徹底的測試:
1)基本功能測試:測試代碼的基本功能;
2)完成白盒覆蓋:在現(xiàn)有用例的基礎(chǔ)上,使用測試用例設(shè)計(jì)器為未覆蓋的語句、條件、分支、路徑設(shè)計(jì)測試用例,達(dá)到100%語句、條件、分支、路徑覆蓋;
3)執(zhí)行自動(dòng)邊界測試捕捉意料之外的錯(cuò)誤。
以上三階段可以由不同人員在不同時(shí)間完成,團(tuán)隊(duì)可以根據(jù)實(shí)際情況做出靈活安排,下面是一個(gè)典型的開發(fā)測試流程。
1)開發(fā)人員邊開發(fā)邊使用VU調(diào)試測試,完成基本功能測試。在VU的支持下開發(fā)調(diào)試,可以大幅提高開發(fā)效率,絕不會(huì)影響開發(fā)進(jìn)度。也許讀者會(huì)認(rèn)為,開發(fā)人員沒有時(shí)間去設(shè)計(jì)測試用例,其實(shí)這是一種誤解,開發(fā)人員寫代碼時(shí)肯定要想清楚代碼的功能并且要使用基本的輸入進(jìn)行調(diào)試,這些就是基本的測試用例,實(shí)際上不需要多做什么。開發(fā)人員提交代碼同時(shí)提交測試代碼和測試報(bào)告。如果項(xiàng)目已完成或部分完成編碼,也建議先由程序員首先對重要代碼進(jìn)行基本的功能測試。
2)測試人員檢查基本測試用例是否符合設(shè)計(jì),并在此基礎(chǔ)上完成白盒覆蓋和邊界測試。由于有了初步的測試,保證了代碼可測性,不可能產(chǎn)生因?yàn)椴划?dāng)耦合造成難于測試的狀況;在現(xiàn)有用例的基礎(chǔ)上,使用測試用例設(shè)計(jì)器找出遺漏用例也不會(huì)有太大障礙,這就使測試人員的工作易于進(jìn)行。測試人員只需要提交更新過的測試代碼和測試報(bào)告,不需要另外記錄測出的問題。
3)開發(fā)人員下載新的測試代碼和測試報(bào)告,執(zhí)行整體測試,然后針對報(bào)告了錯(cuò)誤的函數(shù)執(zhí)行函數(shù)測試以獲取詳細(xì)信息,必要時(shí)進(jìn)行調(diào)試,找出錯(cuò)誤,修改代碼,使所有測試通過,再次提交產(chǎn)品代碼和測試報(bào)告。
4)測試人員再次執(zhí)行整體測試,驗(yàn)證所有的測試均已通過。
以上流程是動(dòng)態(tài)和反復(fù)的,并不是編碼完成后再單元測試。開發(fā)人員寫完一個(gè)類后即可提交代碼由測試人員完成白盒覆蓋和邊界測試。另外,團(tuán)隊(duì)可以根據(jù)開發(fā)與測試人員的比例調(diào)整工作份額,如果團(tuán)隊(duì)沒有測試人員,那么由開發(fā)人員完成全部單元測試也是可行的。
徹底的單元測試對于軟件開發(fā)來說,其價(jià)值是難于估量的:除了保證局部代碼的質(zhì)量外,有了單元測試,任何時(shí)候修改代碼后都可以通過回歸測試來自動(dòng)檢查修改是否引入錯(cuò)誤,這就使開發(fā)過程可以適應(yīng)頻繁變化的需求,系統(tǒng)分析、概要設(shè)計(jì)和詳細(xì)設(shè)計(jì)都可以做得簡單一些,也更能適應(yīng)螺旋式開發(fā)過程,以后的維護(hù)升級(jí)成本也會(huì)大幅降低,同時(shí),高質(zhì)量的代碼使集成測試和系統(tǒng)測試的工作量降低很多(實(shí)際上單元測試已包含了大部分的集成測試),發(fā)現(xiàn)問題后的修改也會(huì)高效得多。總之,要提高軟件開發(fā)質(zhì)量、降低軟件開發(fā)成本,最有效的改進(jìn)就是進(jìn)行徹底的單元測試,如果不進(jìn)行單元測試,任何流程改進(jìn)都無法保證產(chǎn)品質(zhì)量,因?yàn)椋绦蚴冀K是由代碼構(gòu)成的,代碼的質(zhì)量沒有保證,軟件的質(zhì)量拿什么來保證?單元測試并不排斥其他過程改進(jìn),相反,單元測試對開發(fā)流程中的所有環(huán)節(jié)幾乎都有促進(jìn)作用。
下邊再談?wù)勱P(guān)于白盒覆蓋的話題。使用VU實(shí)施單元測試,100%的語句、條件、分支覆蓋通常都是很容易的,路徑覆蓋有時(shí)候會(huì)很難,例如,我們所舉的例子, CExFunction::ParseOneParameter (),有九十多條路徑,要覆蓋似乎不現(xiàn)實(shí)或沒必要,這種狀況通常是設(shè)計(jì)不合理造成的,例如,CExFunction:: ParseOneParameter ()函數(shù)的代碼分為三塊:1、解析缺省值,2、解析數(shù)組,3、解析類型和參數(shù)名,前兩塊解析了缺省值和數(shù)組后把相應(yīng)的Token從列表中刪除,這樣的話,第3塊與前兩塊是沒有邏輯關(guān)系的,但是,這3塊代碼會(huì)組合出很多路徑,沒有邏輯關(guān)系的代碼所組合出來的路徑是沒有意義的,這些代碼具有"高耦合低內(nèi)聚"的特征,不應(yīng)該放在一個(gè)函數(shù)中。范例中另外寫了一個(gè)函數(shù):CExFunction::ParseOneParameter2(),把以上三塊代碼分別獨(dú)立出來自成一個(gè)函數(shù),這樣每個(gè)函數(shù)都能完成100%的路徑覆蓋,重構(gòu)后的代碼既易于測試,也易于維護(hù)。范例中有大量類似的函數(shù),甚至有超過200行的函數(shù),這是為了檢驗(yàn)VU的適應(yīng)能力,以后的版本是會(huì)進(jìn)行重構(gòu)。我們建議程序員完成編碼后,檢查一下路徑數(shù)量,如果路徑很多,代碼很可能需要分拆,合理的路徑數(shù)量應(yīng)該是等價(jià)類數(shù)量的一至兩倍。另外,從邏輯結(jié)構(gòu)圖也可以看出來:圖中有兩個(gè)或兩個(gè)以上串聯(lián)的復(fù)雜分支結(jié)構(gòu),往往表示代碼的結(jié)構(gòu)有問題。
VU的邏輯結(jié)構(gòu)圖具有屏蔽對象的功能,因此,遇到上述情形時(shí)可以通過交替屏蔽部分分支結(jié)構(gòu)的方式來實(shí)現(xiàn)路徑覆蓋,但這不是我們推薦的方式,因?yàn)樗m然可以保證測試的完整性,但并沒有改進(jìn)代碼的結(jié)構(gòu)。
關(guān)于單元測試和范例項(xiàng)目,還有很多值得敘述的話題,例如內(nèi)存泄漏測試以及一些復(fù)雜問題的處理等等,這里暫且不談。最后談?wù)勔淹瓿删幋a的項(xiàng)目的單元測試。對于已完成編碼的項(xiàng)目,最好先由開發(fā)人員"去耦合",方法是將代碼文件從底層向上排列,按順序依次將文件加入到另一個(gè)工程并編譯,如果產(chǎn)生編譯錯(cuò)誤,則想辦法消除編譯錯(cuò)誤 ,VU提供了文件排序工具,具體的使用方法請查閱幫助中《測試舊工程》部分。完成“去耦合”后,由開發(fā)人員對自己編寫的代碼完成基本功能測試,測試人員完成白盒覆蓋和邊界測試。對于編碼過程中使用自然驅(qū)動(dòng)調(diào)試的已完成編寫的代碼,完全由測試部門進(jìn)行單元測試通常是很難的。
來自:http://www.rjzl.gov.cn/news.asp?id=2351