得益於和萱哥關於單元測試的聊天,讓我開始想要了解Google的單元測試框架Google Test,(雖然以前也聽勇哥提到過這個詞,但是一直沒有往心里去);以前基於C#反射特性Demo過一個C#的單元測試框架(http://www.cnblogs.com/salomon/archive/2012/05/30/2526746.html),所以更想明白Google Test測試框架的實現機制;這期間搞過一段Python,看Python文檔時也看到相關的單元測試框架PyUnit,但沒有深入研究,有時間深入研究一下。
自動化測試框架,主要目的就是自動化調用執行某些測試用例,將執行結果與目標結果進行比較,用以達到測試特定目標的目的。而測試用例的針對目標可能是函數接口,功能模塊,UI等等。
自動化測試框架從功能上可以分為技術框架和執行框架。所謂技術框架一般針對於特定的測試目標,為了達到自動化測試而引入的技術或者方法,比如微軟UI測試中的MSAA,UI Automation框架以及后續建立在UIAutomation上的POM/LFM(詳見References中微軟UI自動化測試技術演進相關鏈接)。而執行框架則側重於對測試用例自動化執行的控制。這篇文章主要側重於討論執行框架,而自動化執行框架的設計中有以下幾方面是必須考慮的:
1. 測試用例選擇控制。
2. 自動化傳遞參數或者配置條件。
3. 測試用例執行結果比對。
4. 測試結果記錄與分析。
測試用例選擇控制
測試用例由多人編寫,且針對模塊不同,再加上歷史原因,諸如此類限制情況要選擇特定測試用例執行。C#中常見做法是利用反射機制建立特性標簽,選擇帶有特定標簽的測試用例執行,而C++常見做法是給測試用例加字符串參數,執行時通過字符串參數進行選擇。
自動化傳遞參數或者配置條件
給被測試測試目標傳遞參數或配置文件是自動化測試框架必須有的特性,最可取的做法就是利用XML文檔動態傳參,好處是實現代碼復用(針對同一個測試方法傳遞不同測試參數),以及可以動態自適應測試參數的變化(代碼不改動的情況下調整參數或者配置信息適應測試條件的改變)。
測試用例執行結果比對
測試比對一般代碼中實現測試結果比對,但這涉及到目標結果也就是期望結果的取得的問題,其實和上述傳參和配置條件屬於同一個范疇。
測試結果記錄與分析
結果記錄一般會將結果可視化輸出到CMD窗口或者桌面窗口,XML文件,HTML文件,Excel文件等。不同測試框架實現有所不同,取決於需求。
Google Test
本來想Demo以下Google Test的實現,但是發現網上已經有人做過類似的事情,就把代碼copy過來了,尊重作者意願,將連接放至refereces。
Goolge Test架構主題設計很簡單,將每一個測試用例封裝到一個類TestCase的子類里。一個測試單元類UnitTest中將這些TestCase存入一個Vector的數據結構中,使用RunTestCases()方法(原著中使用Run這個方法名起的不是很好容易與TestCase中Run()方法相混,降低代碼可讀性)控制取出Vector中的測試用例來控制執行過程,結果比對等。
這個架構最出彩的地方是使用宏定義掩蓋了繁雜的測試用例封裝過程,可謂是神來之筆,詳盡代碼請參考連接,於作者博客下載。
class TestCase { public: TestCase(const char* case_name) : testcase_name(case_name){} // 執行測試案例的方法 virtual void Run() = 0; int nTestResult; // 測試案例的執行結果 const char* testcase_name; // 測試案例名稱 };
class UnitTest { public: // 獲取單例 static UnitTest* GetInstance(); // 注冊測試案例 TestCase* RegisterTestCase(TestCase* testcase); // 執行單元測試 int RunTestCases(); TestCase* CurrentTestCase; // 記錄當前執行的測試案例 int nTestResult; // 總的執行結果 int nPassed; // 通過案例數 int nFailed; // 失敗案例數 protected: std::vector<TestCase*> testcases_; // 案例集合 };
// 以下這段宏定義掩蓋了繁雜的測試用例封裝過程
#define TESTCASE_NAME(testcase_name) \
testcase_name##_TEST //##的作用在於(token-pasting)符號連接操作符,即將宏定義的多個形參成一個實際參數名,在這里testcase_name_TEST
#define NANCY_TEST_(testcase_name) \
class TESTCASE_NAME(testcase_name) : public TestCase \
{ \
public: \
TESTCASE_NAME(testcase_name)(constchar* case_name) : TestCase(case_name){}; \
virtualvoid Run(); \
private: \
static TestCase*const testcase_; \
}; \
\
TestCase*const TESTCASE_NAME(testcase_name) \
::testcase_ = UnitTest::GetInstance()->RegisterTestCase( \
new TESTCASE_NAME(testcase_name)(#testcase_name)); \
//#號的作用是(stringizing)字符串化操作符。其作用是:將宏定義中的傳入參數名轉換成用一對雙引號括起來參數名字符串。("testcase_name").
void TESTCASE_NAME(testcase_name)::Run()
/*注意Run()后邊沒有{},之所以這么做是宏定義將測試用例放入到Run的方法主體里。例如
NTEST(FooTest_PassDemo)
{
EXPECT_EQ(3, Foo(1, 2));
EXPECT_EQ(2, Foo(1, 1));
}
上述代碼中EXPECT_EQ(2, Foo(1, 1));代碼放入到Run的方法主體里。
*/
#define NTEST(testcase_name) \
NANCY_TEST_(testcase_name)
#define RUN_ALL_TESTS() \
UnitTest::GetInstance()->RunCases();
#define EXPECT_EQ(m, n) \
if (m != n) \
{ \
UnitTest::GetInstance()->CurrentTestCase->nTestResult = 0; \
std::cout << red << "Failed" << std::endl; \
std::cout << red << "Expect:" << m << std::endl; \
std::cout << red << "Actual:" << n << std::endl; \
}
例如以下測試Foo方法,NTEST(FooTest_PassDemo)就是創建一個名為FooTest_PassDemo_Test的子類,將宏定義的斷言EXPECT_EQ()等放入Run()方法主題中。展開代碼如下。
int Foo(int a, int b) { return a + b; } NTEST(FooTest_PassDemo) { EXPECT_EQ(3, Foo(1, 2)); EXPECT_EQ(2, Foo(1, 1)); } // 將以上宏定義展開等價於以下代碼。 class FooTest_PassDemo_TEST : public TestCase { public: FooTest_PassDemo_TEST(const char* case_name) : TestCase(case_name){}; virtual void Run(); private: static TestCase* const testcase_; }; TestCase* const FooTest_PassDemo_TEST::testcase_ = UnitTest::GetInstance()->RegisterTestCase(new FooTest_PassDemo_TEST("FooTest_PassDemo")); void FooTest_PassDemo_TEST::Run() { if (3 != Foo(1, 2)) { UnitTest::GetInstance()->CurrentTestCase->nTestResult = 0; std::cout << red << "Failed" << std::endl; std::cout << red << "Expect:" << Foo(1, 2) << std::endl; std::cout << red << "Actual:" << 3 << std::endl; } if (2 != Foo(1, 1)) { UnitTest::GetInstance()->CurrentTestCase->nTestResult = 0; std::cout << red << "Failed" << std::endl; std::cout << red << "Expect:" << Foo(1, 1) << std::endl; std::cout << red << "Actual:" << 2 << std::endl; } }
int _tmain(int argc, _TCHAR* argv[])
{
//return UnitTest::GetInstance()->Run();
return RUN_ALL_TESTS();
}
坐在班車上,腦子里回想着這篇博客,突然間有種模糊的意識,COM的設計思想貌似也可以用來設計單元測試框架,沒有成熟的思路,只是個想法,寫下以備忘。
References:
微軟UI自動化測試技術演進:
本文中事例代碼來源:
http://www.cnblogs.com/coderzh/archive/2009/04/12/1434155.html
希望讀者能提供不同看法或者建議。