一個高效的軟體開發過程對軟體開發人員來說是至關重要的,決定着開發是痛苦的掙紮,還是不斷進步的喜悅。國人對軟體藍領的不屑,對繁瑣冗長的傳統開發過程的不耐,使大多數開發人員無所适從,而測試驅動開發(Test-Driven Development)就是為了改善傳統的以實作為目标的軟體開發流程,利用測試來驅動軟體程式的設計和實作,從測試的角度提出的一種全新的開發方式。測試驅動開發可以有效的避免過度設計帶來的浪費,同時也可以讓開發者在開發中擁有更全面的視角,避免過度實作帶來的浪費。是以,測試驅動開發成為極限程式設計中比較流行的一種開發方式,受到很多開發者的青睐。
測試驅動開發的整個過程跟傳統的軟體開發過程有很大的差別,它的基本過程如下:
1) 明确目前要完成的功能。可以記錄成一個 TODO 清單。
2) 快速完成針對此功能的測試用例編寫。
3) 測試代碼編譯不通過。
4) 編寫對應的功能代碼。
5) 測試通過。
6) 對代碼進行重構,并保證測試通過。
7) 循環完成所有功能的開發。
為了保證這一過程能夠快捷友善地進行,通常我們會采用很多開發工具來支援這一過程。在應用最為廣泛的開發工具Visual Studio中,因為有.NET Framework的支援,我們可以很輕松友善地進行C#和Visual Basic語言的測試,是以使用這兩種語言實踐測試驅動開發也很友善。但是,作為Visual Studio中最重要的開發語言的C++,在以往的Visual Studio的版本中也沒法友善地進行測試。如果要實踐面向C++語言的測試驅動開發,我們不得不借助第三方測試工具,比如CPPUnit來幫助進行測試。在整個過程中,我們要使用CPPUnit進行測試,而開發又是在Visual Studio中進行,兩個工具的銜接協作,給測試驅動開發帶來了很多不便。使用測試驅動開發流程的開發人員熱切地盼望有一個面向C++的開發工具可以把測試驅動開發過程中最重要的兩個過程:“測試”和“開發”結合起來,兩者能夠做到無縫銜接,讓測試真正地驅動開發。
幸運的是,開發人員的這一夢想在Visual Studio 2010中成為了現實。全新的Visual Studio 2010已經不僅僅是一個開發工具,它也內建了大量的測試工具,成為一個完整的開發平台。在Visual Studio 2010中,我們可以建立面向C++的測試項目來完成測試驅動開發流程中的測試環節,進而讓整個測試驅動開發過程都在Visual Studio中進行,讓測試和開發做到了無縫銜接,而測試也真正地驅動了開發。
光說不練假把式。為了讓大家更加了解如何在Visual Studio中進行面向C++的測試驅動開發,我們來看一個實際的例子,在例子中體會這一過程是多麼簡單友善。例如,我們要編寫一個計算工資的類Salary,它可以根據員工的入職年份和現在的年份計算整個員工應該得到的工資。
按照測試驅動開發的過程,我們首先設計完成這個Salary類需要實作的功能,為了簡便,我們讓這個類隻需要完成兩個簡單的功能:
1) 能夠給定員工的入職年份,并根據現在的年份給出應得的工資
2) 能夠對錯誤的輸入年份傳回相應的錯誤代碼
既然是測試驅動開發,當然是“開發未動,測試先行”了。按照下面的步驟,首先建立一個測試項目并編寫測試對設計中的功能進行測試:
1) 啟動Visual Studio 2010并建立一個新的“Visual Studio空白解決方案”,方案名字叫做SalarySys。接下來的所有測試和開發都會在這個解決方案中進行。
2) 向剛剛建立的解決方案中添加一個Visual C++測試項目SalaryTest。因為我們的測試需要使用C++/CLI進行編寫以便使用.NET的單元測試架構,是以我們同時要修改測試項目屬性,讓它使用公共語言運作時(/clr)支援。
3) 向測試項目SalaryTest中添加一個單元測試。預設情況下,Visual Studio會為我們建立一個UnitTest1.cpp檔案,在其中我們就可以編寫針對将要實作的工資計算類測試了。
4) 在UnitTest1.cpp檔案中找到“#pragma region Additional test attributes”,在這個區域中,我們編寫一個測試來對Salary的基本功能進行測試。
// 建立測試類的智能指針
// 測試功能設計中的“能夠給定員工的入職年份”
std::unique_ptr pClass(new Salary(2003));
// 測試功能設計中的“根據現在的年份給出應得的工資”
// 判斷函數傳回結果是否符合預期
Assert::AreEqual(1900, pClass->GetSalary(2006));
這裡我們首先建立了一個Salary類的執行個體智能指針,其中構造函數的參數2003表示入職年份,然後調用其GetSalary()函數計算工資,其參數2006表示現在的年份。按照設計的計算規則,其結果應該是1300,這裡我們使用Assert::AreEqual函數對測試結果進行判斷,如果這個斷言函數通過,則表示這個Salary類的測試通過。
除了使用Assert::AreEqual斷言函數對結果進行判斷之外,Visual C++還提供了多種斷言函數,以滿足對不同類型的傳回結果進行判斷的需要。更人性化的是,我們還可以在斷言函數中添加對測試結果的說明,這樣我們更容易以測試的結果來驅動開發。例如:
// 判斷不相等
Assert::AreNotEqual(0, (DWORD_PTR) pClass, "pClass指針不應該為空指針");
// 判斷相等
Assert::AreEqual(0, (DWORD_PTR) pClass, "pClass指針應該為空指針");
// 判斷比較結果是否為true
Assert::IsTrue(pClass == nullptr, "pClass指針應該為空指針");
// 判斷StringValue()傳回的字元串是否與期望的結果相等
Assert::AreEqual("期望的結果", gcnew String(pClass->StringValue());
如果我們現在直接運作這個測試項目,當然是沒法編譯通過的。因為我們還沒有建立Salay類。接下來就是測試驅動開發中的“開發”部分了。為了讓剛才建立的測試項目能夠編譯運作并通過所有的測試,我們需要做的是:
1) 向SalarySys解決方案中新添加一個Visual C++的Win 32靜态庫項目Salary。我們的開發工作就在這個項目中進行。
2) 在Salay項目中新添加一個類Salary以實作工資計算的功能。為了讓測試項目中的測試可以通過,我們先将Salay類簡單地實作如下:
class Salary
{
public:
Salary(int nBaseYear) {};
int GetSalary(int nNow)
return 1300;
}
};
3) 有了開發項目之後,接下來的工作就是将開發項目和測試項目聯系起來,讓測試項目對開發項目進行測試。首先,将開發項目目錄“$(SolutionDir)\Salary\”添加為測試項目的附加包含目錄目錄,這樣測試項目可以找到Salary類的定義。然後我們還要将開發項目這個靜态庫連結到測試項目,為了完成這一步,我們需要在項目屬性中将解決方案的輸出目錄“$(OutputPath)”設定為測試項目的附加庫目錄,然後将靜态庫Salary.lib設定為測試項目的附加依賴項。
完成這些開發項目的編寫以及測試項目的設定之後,我們的測試項目就可以編譯通過并運作其中的測試了。
到這裡,我們就完成了測試驅動開發過程的一次完整的疊代,接下來的工作,就是編寫更多的測試以覆寫設計中的所有用例,然後運作這些測試使這些測試通過,如果測試暫時無法通過,則對開發代碼進行重構實作設計使得所有測試都可以通過。例如,在上面完成第一次開發疊代的基礎,我們可以編寫更多的測試來覆寫Salary類設計中的所有功能點:
[TestMethod]
void TestCalculationn()
int cases[4][2] =
{{2003,1000},{2004,1300},
{2005,1600},{2011,3100}
for(int i = 0; i < 4; ++i)
Assert::AreEqual(cases[i][1], pClass->GetSalary( cases[i][0]));
在這裡我們使用一個數組cases定義了多個測試用例,然後使用for循環對這些用例逐個進行測試。現在我們運作測試項目中的這些測試當然是沒法通過的,是以這些測試就驅動我們去對開發項目中的Salary類進行重構,以使得這些測試可以通過:
Salary(int nBaseYear)
:m_nBaseYear(nBaseYear)
{};
return 300*(nNow - m_nBaseYear) + 1000;
private:
int m_nBaseYear;
對Salary類進行重構之後,所有測試都可以通過了,這樣也就實作了用測試來驅動開發。當然,我們這裡隻是對設計中的第一個功能進行了足夠的測試,完成了第一個功能的開發。我們還可以在這個基礎上編寫更多的測試,進入下一個以測試驅動開發的疊代。我們可以按照上面的過程以測試來驅動第二個功能的實作:
void TestInvalidInput()
// 測試第二個功能點:能夠對錯誤的輸入年份傳回相應的錯誤代碼
Assert::AreEqual(-1, pClass->GetSalary(2001));
// 測試臨界輸入是否傳回正确結果
Assert::AreEqual(1000, pClass->GetSalary(2003));
為了讓這個測試通過,我們必須對Salary進行重構,讓它對錯誤的輸入進行處理并傳回相應的錯誤代碼:
int nYears = nNow - m_nBaseYear;
// 對錯誤的輸入進行處理并傳回相應的錯誤代碼
return -1;
else
// 正确的輸入傳回相應的計算結果