玩轉Google開源C++單元測試框架Google Test系列(gtest)之七 - 深入解析gtest [轉載]

一、前言

“深入解析”對我來說的確有些難度,所以我儘量將我學習到和觀察到的gtest內部實現介紹給大家。本文算是拋磚引玉吧,只能是對gtest的整體結構的一些介紹,想要了解更多細節最好的辦法還是看gtest源碼,如果你看過gtest源碼,你會發現裏面的註釋非常的詳細!好了,下面就開始瞭解gtest吧。

二、從TEST宏開始

前面的文章已經介紹過TEST宏的用法了,通過TEST宏,我們可以非法簡單、方便的編寫測試案例,比如:

TEST(FooTest, Demo)
{
    EXPECT_EQ(
11);
}

 

我們先不去看TEST宏的定義,而是先使用/P參數將TEST展開。如果使用的是Vistual Studio的話:

1. 選中需要展開的代碼文件,右鍵 - 屬性 - C/C++ - Preprocessor

2. Generate Preprocessed File 設置 Without Line Numbers (/EP /P) 或 With Line Numbers (/P)

3. 關閉屬性對話框,右鍵選中需要展開的文件,右鍵菜單中點擊:Compile

編譯過後,會在源代碼目錄生成一個後綴爲.i的文件,比如我對上面的代碼進行展開,展開後的內容爲:

複製代碼
class FooTest_Demo_Test : public ::testing::Test 
{
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"11)))
            ;
        
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. 通過靜態變量test_info_,調用MakeAndRegisterTestInfo對測試案例進行註冊。

如下圖:



上面關鍵的方法就是MakeAndRegisterTestInfo了,我們跳到MakeAndRegisterTestInfo函數中:

複製代碼
// 創建一個 TestInfo 對象並註冊到 Google Test;
// 返回創建的TestInfo對象
//
// 參數:
//
//   test_case_name:            測試案例的名稱
//   name:                           測試的名稱
//   test_case_comment:       測試案例的註釋信息
//   comment:                      測試的註釋信息
//   fixture_class_id:             test fixture類的ID
//   set_up_tc:                    事件函數SetUpTestCases的函數地址
//   tear_down_tc:               事件函數TearDownTestCases的函數地址
//   factory:                        工廠對象,用於創建測試對象(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;
}
複製代碼

 

我們看到,上面創建了一個TestInfo對象,然後通過AddTestInfo註冊了這個對象。TestInfo對象到底是一個什麼樣的東西呢?

TestInfo對象主要用於包含如下信息:

1. 測試案例名稱(testcase name)

2. 測試名稱(test name)

3. 該案例是否需要執行

4. 執行案例時,用於創建Test對象的函數指針

5. 測試結果 

我們還看到,TestInfo的構造函數中,非常重要的一個參數就是工廠對象,它主要負責在運行測試案例時創建出Test對象。我們看到我們上面的例子的factory爲:

new ::testing::internal::TestFactoryImpl< FooTest_Demo_Test>

 

我們明白了,Test對象原來就是TEST宏展開後的那個類的對象(FooTest_Demo_Test),再看看TestFactoryImpl的實現:

template <class TestClass>
class TestFactoryImpl : public TestFactoryBase {
public:
    
virtual Test* CreateTest() { return new TestClass; }
};

 

這個對象工廠夠簡單吧,嗯,Simple is better。當我們需要創建一個測試對象(Test)時,調用factory的CreateTest()方法就可以了。 

創建了TestInfo對象後,再通過下面的方法對TestInfo對象進行註冊:

GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);

 

GetUnitTestImpl()是獲取UnitTestImpl對象:

inline UnitTestImpl* GetUnitTestImpl() {
    
return UnitTest::GetInstance()->impl();
}

 

其中UnitTest是一個單件(Singleton),整個進程空間只有一個實例,通過UnitTest::GetInstance()獲取單件的實例。上面的代碼看到,UnitTestImpl對象是最終是從UnitTest對象中獲取的。那麼UnitTestImpl到底是一個什麼樣的東西呢?可以這樣理解:

UnitTestImpl是一個在UnitTest內部使用的,爲執行單元測試案例而提供了一系列實現的那麼一個類。(自己歸納的,可能不準確)

我們上面的AddTestInfo就是其中的一個實現,負責註冊TestInfo實例:

 

複製代碼
// 添加TestInfo對象到整個單元測試中
//
// 參數:
//
//   set_up_tc:      事件函數SetUpTestCases的函數地址
//   tear_down_tc: 事件函數TearDownTestCases的函數地址
//   test_info:        TestInfo對象
void AddTestInfo(Test::SetUpTestCaseFunc set_up_tc,
               Test::TearDownTestCaseFunc tear_down_tc,
               TestInfo 
* test_info) {
// 處理死亡測試的代碼,先不關注它
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();
    }
}
// 獲取或創建了一個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宏中的兩個參數,第一個參數testcase_name,就是TestCase對象的名稱,第二個參數test_name就是Test對象的名稱。而TestInfo包含了一個測試案例的一系列信息。

2. 一個TestCase對象對應一個或多個TestInfo對象。


 

我們來看看TestCase的創建過程(UnitTestImpl::GetTestCase):

複製代碼
// 查找並返回一個指定名稱的TestCase對象。如果對象不存在,則創建一個並返回
//
// 參數:
//
//   test_case_name:    測試案例名稱
//   set_up_tc:            事件函數SetUpTestCases的函數地址
//   tear_down_tc:       事件函數TearDownTestCases的函數地址
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) {
        
// 沒找到,我們來創建一個
        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宏

我們的測試案例的運行就是通過這個宏發起的。RUN_ALL_TEST的定義非常簡單:

#define RUN_ALL_TESTS()\
    (::testing::UnitTest::GetInstance()
->Run())

 

我們又看到了熟悉的::testing::UnitTest::GetInstance(),看來案例的執行時從UnitTest的Run方法開始的,我提取了一些Run中的關鍵代碼,如下:

複製代碼
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),具體案例該怎麼執行,還是得靠UnitTestImpl

複製代碼
int UnitTestImpl::RunAllTests() {

    
// ...

    printer
->OnUnitTestStart(parent_);

    
// 計時
    const TimeInMillis start = GetTimeInMillis();

    printer
->OnGlobalSetUpStart(parent_);
    
// 執行全局的SetUp事件
    environments_.ForEach(SetUpEnvironment);
    printer
->OnGlobalSetUpEnd(parent_);

    
// 全局的SetUp事件執行成功的話
    if (!Test::HasFatalFailure()) {
        
// 執行每個測試案例
        test_cases_.ForEach(TestCase::RunTestCase);
    }

    
// 執行全局的TearDown事件
    printer->OnGlobalTearDownStart(parent_);
    environments_in_reverse_order_.ForEach(TearDownEnvironment);
    printer
->OnGlobalTearDownEnd(parent_);

    elapsed_time_ 
= GetTimeInMillis() - start;

    
// 執行完成
    printer->OnUnitTestEnd(parent_);

    
// Gets the result and clears it.
    if (!Passed()) {
      failed 
= true;
    }
    ClearResult();

    
// 返回測試結果
    return failed ? 1 : 0;
}
複製代碼

 

上面,我們很開心的看到了我們前面講到的全局事件的調用。environments_是一個Environment的鏈表結構(List),它的內容是我們在main中通過:

testing::AddGlobalTestEnvironment(new FooEnvironment);

 

添加進去的。test_cases_我們之前也瞭解過了,是一個TestCase的鏈表結構(List)。gtest實現了一個鏈表,並且提供了一個Foreach方法,迭代調用某個函數,並將裏面的元素作爲函數的參數:

複製代碼
template <typename F>  // F is the type of the function/functor
void ForEach(F functor) const {
    
for ( const ListNode<E> * node = Head();
          node 
!= NULL;
          node 
= node->next() ) {
      functor(node
->element());
    }
}
複製代碼

 

因此,我們關注一下:environments_.ForEach(SetUpEnvironment),其實是迭代調用了SetUpEnvironment函數:

static void SetUpEnvironment(Environment* env) { env->SetUp(); }

 

最終調用了我們定義的SetUp()函數。

再看看test_cases_.ForEach(TestCase::RunTestCase)的TestCase::RunTestCase實現:

static void RunTestCase(TestCase * test_case) { test_case->Run(); }

 

再看TestCase的Run實現:

複製代碼
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事件在這裏調用
    set_up_tc_();

    
const internal::TimeInMillis start = internal::GetTimeInMillis();
    
// 嗯,前面分析的一個TestCase對應多個TestInfo,因此,在這裏迭代對TestInfo調用RunTest方法
    test_info_list_->ForEach(internal::TestInfoImpl::RunTest);
    elapsed_time_ 
= internal::GetTimeInMillis() - start;

    impl
->os_stack_trace_getter()->UponLeavingGTest();
    
// TearDownTestCases事件在這裏調用
    tear_down_tc_();
    result_printer
->OnTestCaseEnd(this);
    impl
->set_current_test_case(NULL);
}
複製代碼

第二種事件機制又浮出我們眼前,非常興奮。可以看出,SetUpTestCases和TearDownTestCaess是在一個TestCase之前和之後調用的。接着看test_info_list_->ForEach(internal::TestInfoImpl::RunTest):

static void RunTest(TestInfo * test_info) {
    test_info
->impl()->Run();
}

哦?TestInfo也有一個impl?看來我們之前漏掉了點東西,和UnitTest很類似,TestInfo內部也有一個主管各種實現的類,那就是TestInfoImpl,它在TestInfo的構造函數中創建了出來(還記得前面講的TestInfo的創建過程嗎?):

複製代碼
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);
}
複製代碼

 

因此,案例的執行還得看TestInfoImpl的Run()方法,同樣,我簡化一下,只列出關鍵部分的代碼:

複製代碼
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對象創建成功

    if (!Test::HasFatalFailure()) {

        // 調用Test對象的Run()方法,執行測試案例 

        test->Run();
    }

    
// 執行完畢,刪除Test對象
    impl->os_stack_trace_getter()->UponLeavingGTest();
    delete test;
    test 
= NULL;

    // 停止計時
    result_.set_elapsed_time(GetTimeInMillis() - start);
    result_printer->OnTestEnd(parent_);

}
複製代碼

 

上面看到了我們前面講到的對象工廠fatory,通過fatory的CreateTest()方法,創建Test對象,然後執行案例又是通過Test對象的Run()方法:

複製代碼
void Test::Run() {
    
if (!HasSameFixtureClass()) return;

    
internal::UnitTestImpl* const impl = internal::GetUnitTestImpl();
    impl
->os_stack_trace_getter()->UponLeavingGTest();
    __try {
        
// Yeah!每個案例的SetUp事件在這裏調用
        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宏裏的東西終於被調用了!
            TestBody();
        } __except(
internal::UnitTestOptions::GTestShouldProcessSEH(
            GetExceptionCode())) {
            AddExceptionThrownFailure(GetExceptionCode(), 
"the test body");
        }
    }

    impl
->os_stack_trace_getter()->UponLeavingGTest();
    __try {
        
// 每個案例的TearDown事件在這裏調用
        TearDown();
    } __except(
internal::UnitTestOptions::GTestShouldProcessSEH(
        GetExceptionCode())) {
        AddExceptionThrownFailure(GetExceptionCode(), 
"TearDown()");
    }
}
複製代碼

 

上面的代碼裏非常極其以及特別的興奮的看到了執行測試案例的前後事件,測試案例執行TestBody()的代碼。彷彿整個gtest的流程在眼前一目瞭然了。

四、總結

本文通過分析TEST宏和RUN_ALL_TEST宏,瞭解到了整個gtest運作過程,可以說整個過程簡潔而優美。之前讀《代碼之美》,感觸頗深,現在讀過gtest代碼,再次讓我感觸深刻。記得很早前,我對設計的理解是“功能越強大越好,設計越複雜越好,那樣才顯得牛”,漸漸得,我才發現,簡單纔是最好。我曾總結過自己寫代碼的設計原則:功能明確,設計簡單。瞭解了gtest代碼後,猛然發現gtest不就是這樣嗎,同時gtest也給了我很多驚喜,因此,我對gtest的評價是:功能強大,設計簡單,使用方便

總結一下gtest裏的幾個關鍵的對象:

1. UnitTest 單例,總管整個測試,包括測試環境信息,當前執行狀態等等。

2. UnitTestImpl UnitTest內部具體功能的實現者。

3. Test    我們自己編寫的,或通過TEST,TEST_F等宏展開後的Test對象,管理着測試案例的前後事件,具體的執行代碼TestBody。

4. TestCase 測試案例對象,管理着基於TestCase的前後事件,管理內部多個TestInfo。

5. TestInfo  管理着測試案例的基本信息,包括Test對象的創建方法。 

6. TestInfoImpl TestInfo內部具體功能的實現者 。

本文還有很多gtest的細節沒有分析到,比如運行參數,死亡測試,跨平臺處理,斷言的宏等等,希望讀者自己把源碼下載下來慢慢研究。如本文有錯誤之處,也請大家指出,謝謝!

轉載自:http://www.cnblogs.com/coderzh/archive/2009/04/11/1433744.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章