用測試的方法驅動開發,這個概念的提出已經很長時間了,但測試驅動開發在 C 和 C++的應用和實踐卻比較晚,本文用一個簡單項目的實例說明如何在 C 和 C++的開發過程中,應用測試驅動開發的理念,從需求定義,代碼測試案例設計到開發實現這些案例定義的需求,展現了測試驅動開發的魅力。測試驅動開發和現在流行敏捷開發的是分不開的,測試驅動開發是敏捷開發的一個強有力工具,可以幫助我們從簡單的設計開始,逐步地有保護重構設計直至完善設計的過程。
測試驅動開發是 Kent 提出的一種新的軟件開發流程,現在已廣為人知,這種開發方法依賴於極短重復的開發周期,面對開發需求,開發人員要先開發代碼測試用例,這些代碼實現的測試用例定義了工程要實現的需求,然後去開發代碼快速測試通過這這些用例,這個時候的代碼是相對比較粗糙的,只是為了通過這個測試,測試通過以後,這些測試所覆蓋的需求就會相對固定下來了,然後隨著實現更多的需求,以前實現的那些粗糙的代碼的問題會逐步的暴露出來,此時就要用重構來消除重復改進代碼設計,因為自動化的測試用例已經框定了相應的需求,這樣在代碼改進和重構的過程中就不會破壞已實現的需求,實現了安全重構。
從測試驅動開發的流程可以看出來,測試驅動開發僅僅要求一個簡單的設計開始實現需求,然後隨著軟件開發的推進實現有保護重構代碼和設計。依賴於 TDD 開發所生成的單元測試用例代碼,實現有保護重構是大型的軟件開發項目不可以缺少的,代碼級別的測試更能有效地提高軟件產品的質量。測試驅動開發中的重構過程也是一個使設計逐步完善的過程。 本文的主要目的是使測試驅動開發落到實地,和具體的語言(C++)和單元測試框架結合起來,並用實例展示測試驅動開發的魅力。
先開發和設計測試代碼,再代碼實現通過測試,以測試驅動設計實現,開發和設計的過程,得到了快速的反饋,用這些反饋驅動,改進和重構代碼設計,是一個有機的開發過程。按照 Kent 的定義,測試驅動開發的原則是:
這兩個簡單的原則,卻產生了一些復雜的個體和組的行為,這些隱含的技術行為包括:
兩個原則還隱含開發任務的順序:
紅色(Red)-綠色(Green)-重構(Refactor),這個就是測試驅動開發的座右銘(Mantra)。這種開發方式可以有效的減少代碼的缺陷密度,減少 bug 的數量,將大部分的缺陷在代碼的開發過程中消除,減少了 QA 測試和質量保證的成本。
按照軟件工程的說法,軟件缺陷和 bug 發現的越早,所需的更正這些缺陷的成本就會越小。所以在軟件的開發階段,采用測試驅動的開發方法,把測試引入到開發階段,使測試和質量意識融入到開發的過程中,這對提高軟件工程質量非常有幫助。 而且在采用測試驅動開發必然要求所開發的組件、接口、類或方法是可測試的(testable),這就要求開發的組件,接口要遵循組件和類高內聚(Highly Cohesive),組件和組件、類和類之間低耦合(loosely Coupled)原則,這種開發方式生成的代碼必然會幫助開發者,在不斷的有保護重構的過程中,提高軟件架構的設計,使日後的軟件維護變得有章可循。
測試驅動開發符合敏捷軟件開發的精神,在不斷迭代過程中,增量地實現軟件需求而這一切開始可以從簡單設計開始。
C++技術是一種高級語言,它出現的時間要比 Java 和 C#早得多,但支持像 xUnit 框架的 C++單元測試框架發展起來的比較晚。 C++ 的單元測試框架選擇比較多,現在比較流行的 C++測試框架有 Boost Test、UnitTest++、CppTest、Google C++ Testing Framework。 Boost Test,擁有良好的斷言功能,對異常控制,崩潰控制方面處理的比較好,也有良好的可以移植性,但結構復雜,不易於掌握。CPPUnit 是開發比較早的單元測試框架,是對 JUnit 的 C++的移植的一種嘗試,擁有豐富的斷言和期望功能。Google Test C++ 簡稱 Gtest,是近期發展起來的單元測試框架,對 xUnit 支持的比較好,支持 TDD 的紅-綠-重構模式,支持死亡和退出測試,較好的異常測試控制能力,良好的測試報告輸出,擁有自動注冊測試用例和用例分組等功能,還有和 Gmock 框架的無縫結合,支持基於接口的(抽象類的)Mock 測試-模擬測試。
下表是一個對三種流行 C++單元測試框架的簡單比較,Gtest 雖然發展起來的較晚,但豐富功能簡單易用,易學,加之移植性較好,是跨平台項目單元測試框架比較好的選擇。
Mock
測試
通過Gmock
支持
不支持
不支持
易用性
優秀
較復雜
較好
支持類型化的參數化測試
支持
不直接支持
不直接支持
Gtest 是基於 xUnit 的 C++單元測試框架,支持自動化案例自動發掘,豐富的斷言功能,支持用戶自定義斷言,支持死亡測試和退出測試,還有異常測試控制,支持值類型和類型化的參數化測試,接口簡單易用,對每個測試案例有執行時間的輸出,可以幫助分析代碼的執行效率,單一接口文件 gtest.h。
圖 1 是 Console 模式輸出用紅和綠表示失敗和成功的測試用例,看起來比較符合 TDD 的策略和定義
Gtest 的斷言有兩種形式,致命性斷言(Fatal Assertion)和非致命性斷言(Nonfatal Assertion)。
除了基本的斷言形式外,Gtest 還包括一些其他的高級斷言形式,比如死亡斷言,退出斷言測試和異常斷言等。
Gtest 還有其他的一些特性,比如類型參數化測試,值類型參數化的測試,測試用例分組,洗牌式測試等,可以參照附錄中列出的 Gtest 的官網獲取更多的信息。
在測試驅動軟件開發的過程中,我們不可避免的要去依賴第三方系統,比如文件系統、第三方庫、數據庫訪問,其他的在線數據的訪問等,按照測試驅動開發的快速反饋的原則,如果在單元測試用例中去直接訪問這些信息,勢必在測試驅動開發過程中會依賴這些資源從而造成訪問時間無法控制, 所以單元測試一般應該避免直接訪問第三方系統,這就是 Mock 測試的主要目的,用模擬的接口去替換真實的接口,模擬出單元測試需要的第三方數據和接口進而隔離第三方的影響,專注於自己的邏輯實現。Gmock 就是這樣一個 Mock 框架,它是類似於 jMock、EasyMock 和 Hamcres ,但是是 C++版本的 Mock 框架。 Gmock 是基於接口的 Mock 框架,在 C++中接口的定義是通過抽象函數和抽象類來實現的,這種要求勢必會要求我們盡量遵循基於接口的編程原則,把交互界面上的操作抽象成接口,以便是接口可被模擬 Mock。可以在附錄中列出的 Gmock 官網獲取更多信息。