玩转Google开源C++单元测试框架Google Test系列(gtest)之七 - 深入解析gtest
一、前言
“深入解析”對我來說的確有些難度,所以我盡量將我學習到和觀察到的gtest內(nèi)部實現(xiàn)介紹給大家。本文算是拋磚引玉吧,只能是對gtest的整體結(jié)構(gòu)的一些介紹,想要了解更多細節(jié)最好的辦法還是看gtest源碼,如果你看過gtest源碼,你會發(fā)現(xiàn)里面的注釋非常的詳細!好了,下面就開始了解gtest吧。
二、從TEST宏開始
前面的文章已經(jīng)介紹過TEST宏的用法了,通過TEST宏,我們可以非法簡單、方便的編寫測試案例,比如:
{
????EXPECT_EQ(1,?1);
}
?
我們先不去看TEST宏的定義,而是先使用/P參數(shù)將TEST展開。如果使用的是Vistual Studio的話:
1. 選中需要展開的代碼文件,右鍵 - 屬性 - C/C++ - Preprocessor
2. Generate Preprocessed File 設(shè)置 Without Line Numbers (/EP /P) 或 With Line Numbers (/P)
3. 關(guān)閉屬性對話框,右鍵選中需要展開的文件,右鍵菜單中點擊:Compile
編譯過后,會在源代碼目錄生成一個后綴為.i的文件,比如我對上面的代碼進行展開,展開后的內(nèi)容為:
{
public:?
????FooTest_Demo_Test()?{}
private:?
????virtual?void?TestBody();
????static?::testing::TestInfo*?const?test_info_;
????FooTest_Demo_Test(const?FooTest_Demo_Test?&);
????void?operator=(const?FooTest_Demo_Test?&);
};
::testing::TestInfo*?const?FooTest_Demo_Test?
????::test_info_?=?
????????::testing::internal::MakeAndRegisterTestInfo(?
????????????"FooTest",?"Demo",?"",?"",
????????????(::testing::internal::GetTestTypeId()),
????????????::testing::Test::SetUpTestCase,
????????????::testing::Test::TearDownTestCase,
????????????new?::testing::internal::TestFactoryImpl<?FooTest_Demo_Test>);
void?FooTest_Demo_Test::TestBody()
{
????switch?(0)
????case?0:
????????if?(const?::testing::AssertionResult?
????????????????gtest_ar?=?
????????????????????(::testing::internal::?EqHelper<(sizeof(::testing::internal::IsNullLiteralHelper(1))?==?1)>::Compare("1",?"1",?1,?1)))
????????????;
????????else?
????????????::testing::internal::AssertHelper(
????????????????::testing::TPRT_NONFATAL_FAILURE,
????????????????".\\gtest_demo.cpp",
????????????????9,
????????????????gtest_ar.failure_message()
????????????????)?=?::testing::Message();
}
?
展開后,我們觀察到:
1. TEST宏展開后,是一個繼承自testing::Test的類。
2. 我們在TEST宏里面寫的測試代碼,其實是被放到了類的TestBody方法中。
3. 通過靜態(tài)變量test_info_,調(diào)用MakeAndRegisterTestInfo對測試案例進行注冊。
如下圖:
上面關(guān)鍵的方法就是MakeAndRegisterTestInfo了,我們跳到MakeAndRegisterTestInfo函數(shù)中:
//?創(chuàng)建一個?TestInfo?對象并注冊到?Google?Test;//?返回創(chuàng)建的TestInfo對象
//
//?參數(shù):
//
//???test_case_name:? ? ? ? ? ? 測試案例的名稱
//???name:??????????? ? ? ? ? ? ? ?? 測試的名稱
//???test_case_comment: ? ? ? 測試案例的注釋信息
//???comment:????????????????????? 測試的注釋信息
//???fixture_class_id:???????????? test fixture類的ID
//???set_up_tc:??????????????????? 事件函數(shù)SetUpTestCases的函數(shù)地址
//???tear_down_tc:?????????????? 事件函數(shù)TearDownTestCases的函數(shù)地址
//???factory:??????????????????????? 工廠對象,用于創(chuàng)建測試對象(Test)
TestInfo*?MakeAndRegisterTestInfo(
????const?char*?test_case_name,?const?char*?name,
????const?char*?test_case_comment,?const?char*?comment,
????TypeId?fixture_class_id,
????SetUpTestCaseFunc?set_up_tc,
????TearDownTestCaseFunc?tear_down_tc,
????TestFactoryBase*?factory)?{
??TestInfo*?const?test_info?=
??????new?TestInfo(test_case_name,?name,?test_case_comment,?comment,
???????????????????fixture_class_id,?factory);
??GetUnitTestImpl()->AddTestInfo(set_up_tc,?tear_down_tc,?test_info);
??return?test_info;
}
?
我們看到,上面創(chuàng)建了一個TestInfo對象,然后通過AddTestInfo注冊了這個對象。TestInfo對象到底是一個什么樣的東西呢?
TestInfo對象主要用于包含如下信息:
1. 測試案例名稱(testcase name)
2. 測試名稱(test name)
3. 該案例是否需要執(zhí)行
4. 執(zhí)行案例時,用于創(chuàng)建Test對象的函數(shù)指針
5. 測試結(jié)果?
我們還看到,TestInfo的構(gòu)造函數(shù)中,非常重要的一個參數(shù)就是工廠對象,它主要負責在運行測試案例時創(chuàng)建出Test對象。我們看到我們上面的例子的factory為:
?
我們明白了,Test對象原來就是TEST宏展開后的那個類的對象(FooTest_Demo_Test),再看看TestFactoryImpl的實現(xiàn):
class?TestFactoryImpl?:?public?TestFactoryBase?{
public:
????virtual?Test*?CreateTest()?{?return?new?TestClass;?}
};
?
這個對象工廠夠簡單吧,嗯,Simple is better。當我們需要創(chuàng)建一個測試對象(Test)時,調(diào)用factory的CreateTest()方法就可以了。?
創(chuàng)建了TestInfo對象后,再通過下面的方法對TestInfo對象進行注冊:
?
GetUnitTestImpl()是獲取UnitTestImpl對象:
inline?UnitTestImpl*?GetUnitTestImpl()?{????return?UnitTest::GetInstance()->impl();
}
?
其中UnitTest是一個單件(Singleton),整個進程空間只有一個實例,通過UnitTest::GetInstance()獲取單件的實例。上面的代碼看到,UnitTestImpl對象是最終是從UnitTest對象中獲取的。那么UnitTestImpl到底是一個什么樣的東西呢?可以這樣理解:
UnitTestImpl是一個在UnitTest內(nèi)部使用的,為執(zhí)行單元測試案例而提供了一系列實現(xiàn)的那么一個類。(自己歸納的,可能不準確)
我們上面的AddTestInfo就是其中的一個實現(xiàn),負責注冊TestInfo實例:
?
//?添加TestInfo對象到整個單元測試中//
//?參數(shù):
//
//???set_up_tc:??????事件函數(shù)SetUpTestCases的函數(shù)地址
//???tear_down_tc:?事件函數(shù)TearDownTestCases的函數(shù)地址
//???test_info:??????? TestInfo對象
void?AddTestInfo(Test::SetUpTestCaseFunc?set_up_tc,
???????????????Test::TearDownTestCaseFunc?tear_down_tc,
???????????????TestInfo?*?test_info)?{
//?處理死亡測試的代碼,先不關(guān)注它
if?(original_working_dir_.IsEmpty())?{
? ? original_working_dir_.Set(FilePath::GetCurrentDir());
????if?(original_working_dir_.IsEmpty())?{
??????? printf("%s\n",?"Failed?to?get?the?current?working?directory.");
??????? abort();
??? }
}
//?獲取或創(chuàng)建了一個TestCase對象,并將testinfo添加到TestCase對象中。
GetTestCase(test_info->test_case_name(),
????????????test_info->test_case_comment(),
????????????set_up_tc,
????????????tear_down_tc)->AddTestInfo(test_info);
}
我們看到,TestCase對象出來了,并通過AddTestInfo添加了一個TestInfo對象。這時,似乎豁然開朗了:
1. TEST宏中的兩個參數(shù),第一個參數(shù)testcase_name,就是TestCase對象的名稱,第二個參數(shù)test_name就是Test對象的名稱。而TestInfo包含了一個測試案例的一系列信息。
2. 一個TestCase對象對應(yīng)一個或多個TestInfo對象。
?
我們來看看TestCase的創(chuàng)建過程(UnitTestImpl::GetTestCase):
//?查找并返回一個指定名稱的TestCase對象。如果對象不存在,則創(chuàng)建一個并返回
//
//?參數(shù):
//
//???test_case_name:??? 測試案例名稱
//???set_up_tc:????????????事件函數(shù)SetUpTestCases的函數(shù)地址
//???tear_down_tc:???????事件函數(shù)TearDownTestCases的函數(shù)地址
TestCase*?UnitTestImpl::GetTestCase(const?char*?test_case_name,
????????????????????????????????????const?char*?comment,
????????????????????????????????????Test::SetUpTestCaseFunc?set_up_tc,
????????????????????????????????????Test::TearDownTestCaseFunc?tear_down_tc)?{
??//?從test_cases里查找指定名稱的TestCase
? ??internal::ListNode<TestCase*>*?node?=?test_cases_.FindIf(
? ?? ?? TestCaseNameIs(test_case_name));
????if?(node?==?NULL)?{
????????//?沒找到,我們來創(chuàng)建一個
??????? TestCase*?const?test_case?=
?? ?????????new?TestCase(test_case_name,?comment,?set_up_tc,?tear_down_tc);
????????//?判斷是否為死亡測試案例
????????if?(internal::UnitTestOptions::MatchesFilter(String(test_case_name),
?????????????????????????????????????????????????kDeathTestCaseFilter))?{
????????????//?是的話,將該案例插入到最后一個死亡測試案例后
??????????? node?=?test_cases_.InsertAfter(last_death_test_case_,?test_case);
??????????? last_death_test_case_?=?node;
??????? }?else?{
????????????//?否則,添加到test_cases最后。
??????????? test_cases_.PushBack(test_case);
??????????? node?=?test_cases_.Last();
??????? }
??? }
????//?返回TestCase對象
????return?node->element();
}
?
三、回過頭看看TEST宏的定義
#define?TEST(test_case_name,?test_name)\????GTEST_TEST_(test_case_name,?test_name,?\
??????????????::testing::Test,?::testing::internal::GetTestTypeId())
?
同時也看看TEST_F宏
#define?TEST_F(test_fixture,?test_name)\????GTEST_TEST_(test_fixture,?test_name,?test_fixture,?\
??????????????::testing::internal::GetTypeId<test_fixture>())
都是使用了GTEST_TEST_宏,在看看這個宏如何定義的:
#define?GTEST_TEST_(test_case_name,?test_name,?parent_class,?parent_id)\class?GTEST_TEST_CLASS_NAME_(test_case_name,?test_name)?:?public?parent_class?{\
public:\
????GTEST_TEST_CLASS_NAME_(test_case_name,?test_name)()?{}\
private:\
????virtual?void?TestBody();\
????static?::testing::TestInfo*?const?test_info_;\
????GTEST_DISALLOW_COPY_AND_ASSIGN_(\
????????GTEST_TEST_CLASS_NAME_(test_case_name,?test_name));\
};\
\
::testing::TestInfo*?const?GTEST_TEST_CLASS_NAME_(test_case_name,?test_name)\
????::test_info_?=\
????????::testing::internal::MakeAndRegisterTestInfo(\
????????????#test_case_name,?#test_name,?"",?"",?\
????????????(parent_id),?\
????????????parent_class::SetUpTestCase,?\
????????????parent_class::TearDownTestCase,?\
????????????new?::testing::internal::TestFactoryImpl<\
????????????????GTEST_TEST_CLASS_NAME_(test_case_name,?test_name)>);\
void?GTEST_TEST_CLASS_NAME_(test_case_name,?test_name)::TestBody()
?
不需要多解釋了,和我們上面展開看到的差不多,不過這里比較明確的看到了,我們在TEST宏里寫的就是TestBody里的東西。這里再補充說明一下里面的GTEST_DISALLOW_COPY_AND_ASSIGN_宏,我們上面的例子看出,這個宏展開后:
FooTest_Demo_Test(const?FooTest_Demo_Test?&);void?operator=(const?FooTest_Demo_Test?&);
?
正如這個宏的名字一樣,它是用于防止對對象進行拷貝和賦值操作的。?
四、再來了解RUN_ALL_TESTS宏
我們的測試案例的運行就是通過這個宏發(fā)起的。RUN_ALL_TEST的定義非常簡單:
????(::testing::UnitTest::GetInstance()->Run())
?
我們又看到了熟悉的::testing::UnitTest::GetInstance(),看來案例的執(zhí)行時從UnitTest的Run方法開始的,我提取了一些Run中的關(guān)鍵代碼,如下:
int?UnitTest::Run()?{
??? __try?{
????????return?impl_->RunAllTests();
??? }?__except(internal::UnitTestOptions::GTestShouldProcessSEH(
??????? GetExceptionCode()))?{
??????? printf("Exception?thrown?with?code?0x%x.\nFAIL\n",?GetExceptionCode());
??????? fflush(stdout);
????????return?1;
??? }
????return?impl_->RunAllTests();
}
?
我們又看到了熟悉的impl(UnitTestImpl),具體案例該怎么執(zhí)行,還是得靠UnitTestImpl。
int?UnitTestImpl::RunAllTests()?{????//?...
????printer->OnUnitTestStart(parent_);
????//?計時
????const?TimeInMillis?start?=?GetTimeInMillis();
????printer->OnGlobalSetUpStart(parent_);
????//?執(zhí)行全局的SetUp事件
????environments_.ForEach(SetUpEnvironment);
????printer->OnGlobalSetUpEnd(parent_);
????//?全局的SetUp事件執(zhí)行成功的話
????if?(!Test::HasFatalFailure())?{
????????//?執(zhí)行每個測試案例
????????test_cases_.ForEach(TestCase::RunTestCase);
????}
????//?執(zhí)行全局的TearDown事件
????printer->OnGlobalTearDownStart(parent_);
????environments_in_reverse_order_.ForEach(TearDownEnvironment);
????printer->OnGlobalTearDownEnd(parent_);
????elapsed_time_?=?GetTimeInMillis()?-?start;
????//?執(zhí)行完成
????printer->OnUnitTestEnd(parent_);
????//?Gets?the?result?and?clears?it.
????if?(!Passed())?{
??????failed?=?true;
????}
????ClearResult();
????//?返回測試結(jié)果
????return?failed???1?:?0;
}
?
上面,我們很開心的看到了我們前面講到的全局事件的調(diào)用。environments_是一個Environment的鏈表結(jié)構(gòu)(List),它的內(nèi)容是我們在main中通過:
testing::AddGlobalTestEnvironment(new?FooEnvironment);?
添加進去的。test_cases_我們之前也了解過了,是一個TestCase的鏈表結(jié)構(gòu)(List)。gtest實現(xiàn)了一個鏈表,并且提供了一個Foreach方法,迭代調(diào)用某個函數(shù),并將里面的元素作為函數(shù)的參數(shù):
template?<typename?F>??//?F?is?the?type?of?the?function/functorvoid?ForEach(F?functor)?const?{
????for?(?const?ListNode<E>?*?node?=?Head();
??????????node?!=?NULL;
??????????node?=?node->next()?)?{
??????functor(node->element());
????}
}
?
因此,我們關(guān)注一下:environments_.ForEach(SetUpEnvironment),其實是迭代調(diào)用了SetUpEnvironment函數(shù):
static?void?SetUpEnvironment(Environment*?env)?{?env->SetUp();?}?
最終調(diào)用了我們定義的SetUp()函數(shù)。
再看看test_cases_.ForEach(TestCase::RunTestCase)的TestCase::RunTestCase實現(xiàn):
static?void?RunTestCase(TestCase?*?test_case)?{?test_case->Run();?}?
再看TestCase的Run實現(xiàn):
void?TestCase::Run()?{????if?(!should_run_)?return;
????internal::UnitTestImpl*?const?impl?=?internal::GetUnitTestImpl();
????impl->set_current_test_case(this);
????UnitTestEventListenerInterface?*?const?result_printer?=
????impl->result_printer();
????result_printer->OnTestCaseStart(this);
????impl->os_stack_trace_getter()->UponLeavingGTest();
????//?哈!SetUpTestCases事件在這里調(diào)用
????set_up_tc_();
????const?internal::TimeInMillis?start?=?internal::GetTimeInMillis();
????//?嗯,前面分析的一個TestCase對應(yīng)多個TestInfo,因此,在這里迭代對TestInfo調(diào)用RunTest方法
????test_info_list_->ForEach(internal::TestInfoImpl::RunTest);
????elapsed_time_?=?internal::GetTimeInMillis()?-?start;
????impl->os_stack_trace_getter()->UponLeavingGTest();
????//?TearDownTestCases事件在這里調(diào)用
????tear_down_tc_();
????result_printer->OnTestCaseEnd(this);
????impl->set_current_test_case(NULL);
}
第二種事件機制又浮出我們眼前,非常興奮。可以看出,SetUpTestCases和TearDownTestCaess是在一個TestCase之前和之后調(diào)用的。接著看test_info_list_->ForEach(internal::TestInfoImpl::RunTest):
static?void?RunTest(TestInfo?*?test_info)?{????test_info->impl()->Run();
}
哦?TestInfo也有一個impl?看來我們之前漏掉了點東西,和UnitTest很類似,TestInfo內(nèi)部也有一個主管各種實現(xiàn)的類,那就是TestInfoImpl,它在TestInfo的構(gòu)造函數(shù)中創(chuàng)建了出來(還記得前面講的TestInfo的創(chuàng)建過程嗎?):
TestInfo::TestInfo(const?char*?test_case_name,???????????????????const?char*?name,
???????????????????const?char*?test_case_comment,
???????????????????const?char*?comment,
???????????????????internal::TypeId?fixture_class_id,
???????????????????internal::TestFactoryBase*?factory)?{
????impl_?=?new?internal::TestInfoImpl(this,?test_case_name,?name,
?????????????????????????????????????test_case_comment,?comment,
?????????????????????????????????????fixture_class_id,?factory);
}
?
因此,案例的執(zhí)行還得看TestInfoImpl的Run()方法,同樣,我簡化一下,只列出關(guān)鍵部分的代碼:
void?TestInfoImpl::Run()?{
????//?...
??? UnitTestEventListenerInterface*?const?result_printer?=??????? impl->result_printer();
??? result_printer->OnTestStart(parent_);
????//?開始計時
????const?TimeInMillis?start?=?GetTimeInMillis();
? ? Test*?test?=?NULL;
??? __try?{
????????//?我們的對象工廠,使用CreateTest()生成Test對象
??????? test?=?factory_->CreateTest();
??? }?__except(internal::UnitTestOptions::GTestShouldProcessSEH(
??????? GetExceptionCode()))?{
??????? AddExceptionThrownFailure(GetExceptionCode(),
??????????????????????????????"the?test?fixture's?constructor");
????????return;
??? }
????//?如果Test對象創(chuàng)建成功????if?(!Test::HasFatalFailure())?{
? ? ? ??// 調(diào)用Test對象的Run()方法,執(zhí)行測試案例?
??? }
????//?執(zhí)行完畢,刪除Test對象
??? impl->os_stack_trace_getter()->UponLeavingGTest();
??? delete?test;
??? test?=?NULL;
????//?停止計時
??? result_.set_elapsed_time(GetTimeInMillis()?-?start);
??? result_printer->OnTestEnd(parent_);
}
?
上面看到了我們前面講到的對象工廠fatory,通過fatory的CreateTest()方法,創(chuàng)建Test對象,然后執(zhí)行案例又是通過Test對象的Run()方法:
void?Test::Run()?{
????if?(!HasSameFixtureClass())?return;
????internal::UnitTestImpl*?const?impl?=?internal::GetUnitTestImpl();
????impl->os_stack_trace_getter()->UponLeavingGTest();
????__try?{
????????//?Yeah!每個案例的SetUp事件在這里調(diào)用
????????SetUp();
????}?__except(internal::UnitTestOptions::GTestShouldProcessSEH(
????????GetExceptionCode()))?{
????????AddExceptionThrownFailure(GetExceptionCode(),?"SetUp()");
????}
????//?We?will?run?the?test?only?if?SetUp()?had?no?fatal?failure.
????if?(!HasFatalFailure())?{
????????impl->os_stack_trace_getter()->UponLeavingGTest();
????????__try?{
????????????//?哈哈!!千辛萬苦,我們定義在TEST宏里的東西終于被調(diào)用了!
????????????TestBody();
????????}?__except(internal::UnitTestOptions::GTestShouldProcessSEH(
????????????GetExceptionCode()))?{
????????????AddExceptionThrownFailure(GetExceptionCode(),?"the?test?body");
????????}
????}
????impl->os_stack_trace_getter()->UponLeavingGTest();
????__try?{
????????//?每個案例的TearDown事件在這里調(diào)用
????????TearDown();
????}?__except(internal::UnitTestOptions::GTestShouldProcessSEH(
????????GetExceptionCode()))?{
????????AddExceptionThrownFailure(GetExceptionCode(),?"TearDown()");
????}
}
?
上面的代碼里非常極其以及特別的興奮的看到了執(zhí)行測試案例的前后事件,測試案例執(zhí)行TestBody()的代碼。仿佛整個gtest的流程在眼前一目了然了。
四、總結(jié)
本文通過分析TEST宏和RUN_ALL_TEST宏,了解到了整個gtest運作過程,可以說整個過程簡潔而優(yōu)美。之前讀《代碼之美》,感觸頗深,現(xiàn)在讀過gtest代碼,再次讓我感觸深刻。記得很早前,我對設(shè)計的理解是“功能越強大越好,設(shè)計越復雜越好,那樣才顯得牛”,漸漸得,我才發(fā)現(xiàn),簡單才是最好。我曾總結(jié)過自己寫代碼的設(shè)計原則:功能明確,設(shè)計簡單。了解了gtest代碼后,猛然發(fā)現(xiàn)gtest不就是這樣嗎,同時gtest也給了我很多驚喜,因此,我對gtest的評價是:功能強大,設(shè)計簡單,使用方便。
總結(jié)一下gtest里的幾個關(guān)鍵的對象:
1. UnitTest 單例,總管整個測試,包括測試環(huán)境信息,當前執(zhí)行狀態(tài)等等。
2. UnitTestImpl UnitTest內(nèi)部具體功能的實現(xiàn)者。
3. Test ?? 我們自己編寫的,或通過TEST,TEST_F等宏展開后的Test對象,管理著測試案例的前后事件,具體的執(zhí)行代碼TestBody。
4. TestCase 測試案例對象,管理著基于TestCase的前后事件,管理內(nèi)部多個TestInfo。
5. TestInfo? 管理著測試案例的基本信息,包括Test對象的創(chuàng)建方法。?
6. TestInfoImpl TestInfo內(nèi)部具體功能的實現(xiàn)者 。
本文還有很多gtest的細節(jié)沒有分析到,比如運行參數(shù),死亡測試,跨平臺處理,斷言的宏等等,希望讀者自己把源碼下載下來慢慢研究。如本文有錯誤之處,也請大家指出,謝謝!
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的玩转Google开源C++单元测试框架Google Test系列(gtest)之七 - 深入解析gtest的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 玩转Google开源C++单元测试框架G
- 下一篇: 玩转Google开源C++单元测试框架G