本文寫于05年,是我關(guān)于單元測試的第一篇文章。讀者和轉(zhuǎn)載都很多,不過經(jīng)過更豐富的實踐尤其是涉及了不同企業(yè)的眾多項目的單元測試工作后,今天看來,文中的一些觀點是不正確的, 例如關(guān)于樁代碼的說法。近期我會多寫一些基于企業(yè)應(yīng)用的相關(guān)理論和方法介紹。這篇文章對于初學(xué)者理解單元測試還是不錯的。
一 單元測試概述
工廠在組裝一臺電視機之前,會對每個元件都進行測試,這,就是單元測試。
其實我們每天都在做單元測試。你寫了一個函數(shù),除了極簡單的外,總是要執(zhí)行一下,看看功能是否正常,有時還要想辦法輸出些數(shù)據(jù),如彈出信息窗口什么的,這,也是單元測試,老納把這種單元測試稱為臨時單元測試。只進行了臨時單元測試的軟件,針對代碼的測試很不完整,代碼覆蓋率要超過70%都很困難,未覆蓋的代碼可能遺留大量的細(xì)小的錯誤,這些錯誤還會互相影響,當(dāng)BUG暴露出來的時候難于調(diào)試,大幅度提高后期測試和維護成本,也降低了開發(fā)商的競爭力??梢哉f,進行充分的單元測試,是提高軟件質(zhì)量,降低開發(fā)成本的必由之路。
對于程序員來說,如果養(yǎng)成了對自己寫的代碼進行單元測試的習(xí)慣,不但可以寫出高質(zhì)量的代碼,而且還能提高編程水平。
要進行充分的單元測試,應(yīng)專門編寫測試代碼,并與產(chǎn)品代碼隔離。老納認(rèn)為,比較簡單的辦法是為產(chǎn)品工程建立對應(yīng)的測試工程,為每個類建立對應(yīng)的測試類,為每個函數(shù)(很簡單的除外)建立測試函數(shù)。首先就幾個概念談?wù)劺霞{的看法。
一般認(rèn)為,在結(jié)構(gòu)化程序時代,單元測試所說的單元是指函數(shù),在當(dāng)今的面向?qū)ο髸r代,單元測試所說的單元是指類。以老納的實踐來看,以類作為測試單位,復(fù)雜度高,可操作性較差,因此仍然主張以函數(shù)作為單元測試的測試單位,但可以用一個測試類來組織某個類的所有測試函數(shù)。單元測試不應(yīng)過分強調(diào)面向?qū)ο?,因為局部代碼依然是結(jié)構(gòu)化的。單元測試的工作量較大,簡單實用高效才是硬道理。
有一種看法是,只測試類的接口(公有函數(shù)),不測試其他函數(shù),從面向?qū)ο蠼嵌葋砜矗_實有其道理,但是,測試的目的是找錯并最終排錯,因此,只要是包含錯誤的可能性較大的函數(shù)都要測試,跟函數(shù)是否私有沒有關(guān)系。對于C++來說,可以用一種簡單的方法區(qū)隔需測試的函數(shù):簡單的函數(shù)如數(shù)據(jù)讀寫函數(shù)的實現(xiàn)在頭文件中編寫(inline函數(shù)),所有在源文件編寫實現(xiàn)的函數(shù)都要進行測試(構(gòu)造函數(shù)和析構(gòu)函數(shù)除外)。
什么時候測試?單元測試越早越好,早到什么程度?XP開發(fā)理論講究TDD,即測試驅(qū)動開發(fā),先編寫測試代碼,再進行開發(fā)。在實際的工作中,可以不必過分強調(diào)先什么后什么,重要的是高效和感覺舒適。從老納的經(jīng)驗來看,先編寫產(chǎn)品函數(shù)的框架,然后編寫測試函數(shù),針對產(chǎn)品函數(shù)的功能編寫測試用例,然后編寫產(chǎn)品函數(shù)的代碼,每寫一個功能點都運行測試,隨時補充測試用例。所謂先編寫產(chǎn)品函數(shù)的框架,是指先編寫函數(shù)空的實現(xiàn),有返回值的隨便返回一個值,編譯通過后再編寫測試代碼,這時,函數(shù)名、參數(shù)表、返回類型都應(yīng)該確定下來了,所編寫的測試代碼以后需修改的可能性比較小。
由誰測試?單元測試與其他測試不同,單元測試可看作是編碼工作的一部分,應(yīng)該由程序員完成,也就是說,經(jīng)過了單元測試的代碼才是已完成的代碼,提交產(chǎn)品代碼時也要同時提交測試代碼。測試部門可以作一定程度的審核。
關(guān)于樁代碼,老納認(rèn)為,單元測試應(yīng)避免編寫樁代碼。樁代碼就是用來代替某些代碼的代碼,例如,產(chǎn)品函數(shù)或測試函數(shù)調(diào)用了一個未編寫的函數(shù),可以編寫樁函數(shù)來代替該被調(diào)用的函數(shù),樁代碼也用于實現(xiàn)測試隔離。采用由底向上的方式進行開發(fā),底層的代碼先開發(fā)并先測試,可以避免編寫樁代碼,這樣做的好處有:減少了工作量;測試上層函數(shù)時,也是對下層函數(shù)的間接測試;當(dāng)下層函數(shù)修改時,通過回歸測試可以確認(rèn)修改是否導(dǎo)致上層函數(shù)產(chǎn)生錯誤。
二 測試代碼編寫
多數(shù)講述單元測試的文章都是以Java為例,本文以C++為例,后半部分所介紹的單元測試工具也只介紹C++單元測試工具。下面的示例代碼的開發(fā)環(huán)境是VC6.0。
產(chǎn)品類:
class CMyClass
{
public:
int Add(int i, int j);
CMyClass();
virtual ~CMyClass();
private:
int mAge; //年齡
CString mPhase; //年齡階段,如"少年","青年"
};
建立對應(yīng)的測試類CMyClassTester,為了節(jié)約編幅,只列出源文件的代碼:
void CMyClassTester::CaseBegin()
{
//pObj是CMyClassTester類的成員變量,是被測試類的對象的指針,
//為求簡單,所有的測試類都可以用pObj命名被測試對象的指針。
pObj = new CMyClass();
}
void CMyClassTester::CaseEnd()
{
delete pObj;
}
測試類的函數(shù)CaseBegin()和CaseEnd()建立和銷毀被測試對象,每個測試用例的開頭都要調(diào)用CaseBegin(),結(jié)尾都要調(diào)用CaseEnd()。
接下來,我們建立示例的產(chǎn)品函數(shù):
int CMyClass::Add(int i, int j)
{
return i+j;
}
和對應(yīng)的測試函數(shù):
void CMyClassTester::Add_int_int()
{
}
把參數(shù)表作為函數(shù)名的一部分,這樣當(dāng)出現(xiàn)重載的被測試函數(shù)時,測試函數(shù)不會產(chǎn)生命名沖突。下面添加測試用例:
void CMyClassTester::Add_int_int()
{
//第一個測試用例
CaseBegin();{ //1
int i = 0; //2
int j = 0; //3
int ret = pObj->Add(i, j); //4
ASSERT(ret == 0); //5
}CaseEnd(); //6
}
第1和第6行建立和銷毀被測試對象,所加的{}是為了讓每個測試用例的代碼有一個獨立的域,以便多個測試用例使用相同的變量名。
第2和第3行是定義輸入數(shù)據(jù),第4行是調(diào)用被測試函數(shù),這些容易理解,不作進一步解釋。第5行是預(yù)期輸出,它的特點是當(dāng)實際輸出與預(yù)期輸出不同時自動報錯,ASSERT是VC的斷言宏,也可以使用其他類似功能的宏,使用測試工具進行單元測試時,可以使用該工具定義的斷言宏。
示例中的格式顯得很不簡潔,2、3、4、5行可以合寫為一行:ASSERT(pObj->Add(0, 0) == 0);但這種不簡潔的格式卻是老納極力推薦的,因為它一目了然,易于建立多個測試用例,并且具有很好的適應(yīng)性,同時,也是極佳的代碼文檔,總之,老納建議:輸入數(shù)據(jù)和預(yù)期輸出要自成一塊。
建立了第一個測試用例后,應(yīng)編譯并運行測試,以排除語法錯誤,然后,使用拷貝/修改的辦法建立其他測試用例。由于各個測試用例之間的差別往往很小,通常只需修改一兩個數(shù)據(jù),拷貝/修改是建立多個測試用例的最快捷辦法。
新聞熱點
疑難解答