在正式開啟GoogleTest之旅前,先介紹一點術語,以便平滑過渡。
所謂黑盒,將待測對象看成一個黑盒子,測試時不關心功能是如何實現的,僅關注輸入數據和實際輸出結果,核對實際輸出是否與預期輸出一致。
功能測試採用的就是黑盒測試技術,如下就是一條手機的功能測試用例,測試步驟對應輸入(點擊圖標)、預期結果對應輸出(打開應用)。
所謂白盒,簡單來說就是代碼層面的測試。測試人員需要了解功能是如何編碼實現的(需要讀懂代碼)。
單元測試就是代碼層面的測試,舉個例子。Absolute( )函數實現了求整數絕對值的功能:
int Absolute(int n) { if (n > 0) return n; else if (n < 0) return n * -1; }(左右滑動查看完整代碼)
瀏覽Absolute( )函數,可以看出它的代碼存在明顯bug:沒有考慮輸入為0的情況。
運行程序,當輸入數據為0時,輸出結果是錯誤的:
通過上面的示例,相信大家對單元測試有了一個比較直觀的印象了,下面給出單元測試的定義。
產品代碼中的 一個函數、一個類 或 一個接口均可以看成一個單元,針對這些單元的代碼級測試就是單元測試。
單元測試的對象是一個個「單元」,單元測試可以發現待測對象中的代碼級故障,對產品整體性錯誤無能為力。
雖然單元測試有其局限性,但是良好的單元測試可以保障一個單元模塊的代碼正確性,即:該單元被其他模塊調用時,自身是沒有代碼問題的。
GoogleTest 是Google公司開發的一款C++單元測試框架,Google Chrome瀏覽器使用的測試框架就是GoogleTest。
對於使用C++開發的產品,可以通過GTest編寫單元測試用例進行單元測試。
為什麼要編寫單元測試用例,上面的Absolute( )函數進行代碼走查不就搞定了嗎?
光是走查(沒有對應的單元測試用例),當代碼變更後,需要重新走查,之前的走查成果無法繼承。
隨著單元模塊邏輯複雜度的提升,必須編寫測試代碼進行代碼質量的保障(大神除外)。
對於擁有單元測試用例的模塊,當此單元進行較大的代碼優化後,可以通過已有的單元測試用例快速評估優化後的代碼質量、及時發現代碼錯誤。
為什麼要使用測試框架,直接單元測試不香嗎?先說結論:不香,直接編寫測試用例進行單元測試整體效率要低得多。
測試框架是對整個測試系統的可重複使用設計,可重複意味著自動化。有了測試框架,測試人員不再需要編寫瑣碎的測試代碼,從而可以專注於測試用例本身,使得測試更聚焦。
好的單元測試框架及Google Test的優勢(摘自GoogleTest Primer.md,你理解為GTest自己吹捧自己也沒錯)。
GTest使每個測試用例運行在不同的對象中從而使測試隔離。
當一個測試失敗時,GTest允許你將它運行在隔離的環境下從而達到快速調試的目的。
測試有良好的組織,可以反映被測試代碼的結構。GTest將相關測試劃分到一個測試組內,組內的測試能共享數據,使測試易於維護。
與平臺無關的代碼,其測試代碼也應該和平臺無關,GTest能在不同的OS下工作,並且支持不同的編譯器。
當用例執行失敗時,可以提供儘可能多的有效信息,以便定位問題。
GTest可以定製出錯時的有效信息,使得更容易定位故障。
GTest能在測試用例之間復用測試資源,使單元測試更高效。
囉嗦了這麼久,下面正式開始。
注意:所有示例均已在Windows10+Visual Studio 2019上調試通過,函數到接口的單元測試示例為GoogleTest的附帶示例,作者進行了標註並結合實際情況進行了少量改動。
Windows10 + Visual Studio 2019(vs2019已集成GTest)。
1.打開VS 2019,創建新項目。
2.搜索關鍵字 Google,可以得到 Google Test的項目模板:Write C++ unit tests using Google Test。
3.選擇項目模板Google Test,點擊下一步。
4.配置項目名稱和位置,點擊創建。
5.測試項目配置如下:
6.等待VS創建好新項目:如圖所示,創建好了名為myGTest的項目。
1.在test.cpp中編寫函數Factorial() ,並編寫Factorial( ) 對應的單元測試用例(Factorial( )函數用於計算整數n的階乘)。
2. 觀察測試用例TEST(TestFactorialFunc, FirstGTest) 。
(1) 編寫測試用例使用了TEST宏,它有兩個參數:TEST[TestCaseName,TestName],TestCaseName對應測試用例集名稱,TestName是歸屬的測試用例名稱。
(2)對檢查點的檢查,使用了EXPECT_EQ宏,用來比較兩個數字是否相等。
可以看到,通過GTest編寫用例還是蠻方便的。
Google打包了一系列EXPECT_* 和 ASSERT_* 宏,EXPECT和ASSERT的區別:
1. 在VS2019中創建新項目 Practice(待測項目)。
其中的Practice.cpp包含main( )函數:
#include <iostream> using namespace std; int main() { cout << "Start Google Test…\n"; return 0;}(左右滑動查看完整代碼)
2. 創建Practice項目配套的gtest項目。
(1)文件-新建-項目,打開「創建新項目」窗口。
(2)選擇創建Google Test,然後點擊下一步。
(3)配置新項目 窗口:項目名稱自定,解決方案選擇是添加到解決方案。然後點擊創建:
(5)如圖所示,已經創建好了Practice項目 對應的gtest項目practice_gtest。
1.設置項目之間的依賴,可以簡化頭文件的路徑描述:
2.設置依賴關係
(1) 右鍵點擊Practice_gtest,選擇屬性。
(2) 在屬性頁中,依次定位到:配置屬性 --- C/C++ --- 常規 --- 附加包含目錄,點擊下拉框後選擇編輯。
(3) 在彈出的附加包含目錄窗口中設定依賴關係:輸入待測項目的目錄地址。
1.在practice項目中新建文件 Calc.h、Calc.cpp,用於模擬加減乘除運算。
class Calc { public: int Add(int a, int b); int Minus(int a, int b); int Multi(int a, int b); float Divide(float a, float b); };(左右滑動查看完整代碼)
#include "Calc.h" int Calc::Add(int a, int b) return a + b; int Calc::Minus(int a, int b) return a - b; int Calc::Multi(int a, int b) return a * b; float Calc::Divide(float a, float b) return a / b;(左右滑動查看完整代碼)
2.在gtest測試工程中新建ClassCalcTest.cpp,用於測試類Calc。
這裡使用的還是TEST宏,測試用例分別對應代碼中的加減乘除。
一個問題:為什麼測試對象是 Calc.h,而不是Calc.cpp?
因為調用的是接口(*.h頭文件),而不是實現(*.cpp文件):
#include "pch.h" #include "Calc.h" Calc calculation; TEST(CalcClassTest, add) { EXPECT_EQ(3,calculation.Add(1, 2)); } TEST(CalcClassTest, minus) { EXPECT_EQ(calculation.Minus(1, 2), -1); } TEST(CalcClassTest, multi) { EXPECT_EQ(calculation.Multi(1, 2), 2); } TEST(CalcClassTest, devide) { EXPECT_FLOAT_EQ(calculation.Divide(1, 2),0.5); }(左右滑動查看完整代碼)
3.正式啟動測試前,需要先設定好對應目標文件的地址。
(1) 進入測試工程Practice_gtest的屬性設定界面(右鍵點擊項目,在彈出菜單中選擇屬性)。
(2) 依次定位到連結器-輸入-附加依賴項,點擊下拉框,進行編輯。
(3) 輸入類Calc的目標文件 Calc.obj的地址。
注意:當測試多個類時,需要分別添加對應的obj文件,該場景下不能使用通配符 * ,否則執行測試時會報錯。
代碼文件是 sample01.h、sample01.cpp,對應的單元測試文件是sample01UnitTest.cpp。
sample01.h進行了函數聲明:
int Factorial(int n); bool IsPrime(int n);(左右滑動查看完整代碼)
sample01.cpp中撰寫了待測試的函數(對應的接口文件是sample01.h):Factorial( )函數用於求一個數的階乘,IsPrime( )函數用於判定一個數是否是素數:
#include "sample01.h" int Factorial(int n) { int result = 1; for (int i = 1; i <= n; i++) result *= i; return result; } bool IsPrime(int n) { if (n <= 1) return false; if (n % 2 == 0) return n == 2; for (int i = 3; ; i += 2) { if (i > n / i) break; if (n % i == 0) return false; } return true; }(左右滑動查看完整代碼)
注意:編寫單元測試用例時,你的重點並不是讀懂每一行原碼,而是弄清楚這個單元對應的主體功能、調用方式 以及 代碼的邏輯構成。
你的目標是:編寫的單元測試用例可以覆蓋所有的代碼邏輯分支。
在單元測試用例sample01UnitTest.cpp中,可以看到他覆蓋了待測函數的各個分支(請參考下圖)。
大家可以通過閱讀下面的源碼和注釋加深理解:
TEST(FactorialTest, Negative) { EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_GT(Factorial(-10), 0); } TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); } TEST(FactorialTest, Positive) { EXPECT_EQ(1, Factorial(1)); EXPECT_EQ(2, Factorial(2)); EXPECT_EQ(6, Factorial(3)); EXPECT_EQ(40320, Factorial(8)); } TEST(IsPrimeTest, Negative) { EXPECT_FALSE(IsPrime(-1)); EXPECT_FALSE(IsPrime(-2)); EXPECT_FALSE(IsPrime(INT_MIN)); } TEST(IsPrimeTest, Trivial) { EXPECT_FALSE(IsPrime(0)); EXPECT_FALSE(IsPrime(1)); EXPECT_TRUE(IsPrime(2)); EXPECT_TRUE(IsPrime(3)); } TEST(IsPrimeTest, Positive) { EXPECT_FALSE(IsPrime(4)); EXPECT_TRUE(IsPrime(5)); EXPECT_FALSE(IsPrime(6)); EXPECT_TRUE(IsPrime(23)); }(左右滑動查看完整代碼)
類測試、接口測試以及Test Fixture的使用將在下面幾篇介紹~