解析gtest框架運行機制


前言

Google test是一款開源的白盒單元測試框架,據說目前在Google內部已在幾千個項目中應用了基於該框架的白盒測試。

最近的工作是在搞一個基於gtest框架搭建的自動化白盒測試項目,該項目上線也有一段時間了,目前來說效果還是挺不錯的。

侯捷先生在《STL源碼剖析》中說過一句話:”會用STL,是一種檔次。對STL原理有所了解,又是一個檔次。追蹤過STL源碼又是一個檔次。第三種檔次的人用起STL來,虎虎生風之勢絕非第一檔次的人能夠望其項背。“

我認為使用一種框架時也是一樣,只有當你知道框架內部是如何運行的,不僅知其然,還知其所以然,才能避免一些坑,使框架用起來更效率。

就拿平常項目中用的最簡單的一個測試demo(test_foo.cpp)來說吧

int foo(int a, int b)
{
    return a + b;
}

class TestWidget : public testing::Environment
{
public:
    virtual void SetUp();
    virtual void TearDown();
};

TEST(Test_foo, test_normal)
{
    EXPECT_EQ(2, foo(1, 1));  
}

int main(int argc, char const *argv[])
{
    testing::AddGlobalTestEnvironment(new TestSysdbg);
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
    return 0;
}

你可知道gtest是如何調用被測接口,如何輸出測試結果的嗎?本文主要解答這個問題。

解析

從預處理開始

可能有些童鞋會問,為什么要從預處理開始講呢?是這樣的,用過gtest框架的都知道,我們所編寫的每一個測試用例都是一個TEST宏,想知道背后的運行機制,就得知道那些TEST宏展開后是什么樣的,而所有的宏、包含的頭文件、inline函數都是在預處理階段被預處理器展開的,然后才是經過編譯器編譯成匯編代碼,接着匯編器把匯編代碼生成可重定位的目標文件,然后鏈接器再把可重定位的目標文件鏈接成可執行文件的目標文件。

所以本文從預處理開始介紹。

需要提到的是Gtest中用到了許多的宏技巧以及c++的模板技巧。先不看源碼中TEST宏的定義,直接用下面指令單獨調用預處理器對源文件進行預處理:

cpp test_foo.cpp test_foo.i –I/ gtest/gtest-1.6/

打開生成的經過預處理的文件test_foo.i

class Test_foo_test_normal_Test : public ::testing::Test 
{ 
public: 
    Test_foo_test_normal_Test() {} 
 
private: 
    virtual void TestBody();
public:
    virtual void SetUp();
    virtual void TearDown();
};

class Test_foo_test_normal_Test : public ::testing::Test 
{ 
public: 
    Test_foo_test_normal_Test() {} 

private: 
    virtual void TestBody();
    static ::testing::TestInfo* const test_info_ __attribute__ ((unused)); 
    Test_foo_test_normal_Test(Test_foo_test_normal_Test const &); 
    void operator=(Test_foo_test_normal_Test const &);
};

::testing::TestInfo* const Test_foo_test_normal_Test 
  ::test_info_ = 
    ::testing::internal::MakeAndRegisterTestInfo( 
      "Test_foo", "test_normal", __null, __null, 
        (::testing::internal::GetTestTypeId()), 
        ::testing::Test::SetUpTestCase, 
        ::testing::Test::TearDownTestCase, 
new ::testing::internal::TestFactoryImpl<Test_foo_test_normal_Test>);

void Test_foo_test_normal_Test::TestBody()
{
  switch (0) 
    case 0: 
    default: 
      if (const ::testing::AssertionResult gtest_ar = 
        (::testing::internal:: 
        EqHelper<(sizeof(::testing::internal::IsNullLiteralHelper(2)) == 1) > 
        ::Compare("2", "foo(1, 1)", 2, foo(1, 1)))) ; 
      else 
        ::testing::internal::AssertHelper(::testing::TestPartResult::kNonFatalFailure, 
        "test_foo.cpp", 17, gtest_ar.failure_message()) = ::testing::Message();
}

int main(int argc, char *argv[])
{
    testing::AddGlobalTestEnvironment(new TestWidget);
    testing::InitGoogleTest(&argc, argv);
    return (::testing::UnitTest::GetInstance()->Run());
    return 0;
}

可以看到TEST宏經過預處理器處理后展開為:

  • 定義了一個繼承自::testing::test類的新類Test_foo_test_normal_Test,該類的名字為TEST宏兩個形參的拼接而成。
  • TEST宏中的測試代碼被展開並定義為生成類的成員函數TestBody的函數體。
  • 生成類的靜態數據成員test_info_被初始化為函MakeAndRegisterTestInfo的返回值。具體意義后面介紹。

MakeAndRegisterTestInfo函數

從上面來看MakeAndRegisterTestInfo函數是一個比較關鍵的函數了,從字面意思上看就是生成並注冊該測試案例的信息,在頭文件gtest.cc中可以找到關於它的定義,他是一個testing命名空間中的嵌套命名空間internal中的非成員函數:

TestInfo* MakeAndRegisterTestInfo(
    const char* test_case_name, const char* name,
    const char* type_param,
    const char* value_param,
    TypeId fixture_class_id,
    SetUpTestCaseFunc set_up_tc,
    TearDownTestCaseFunc tear_down_tc,
    TestFactoryBase* factory) {
  TestInfo* const test_info =
      new TestInfo(test_case_name, name, type_param, value_param,
                   fixture_class_id, factory);
  GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
  return test_info;
}

其中形參的意義如下:

  • test_case_name:測試套名稱,即TEST宏中的第一個形參。
  • name:測試案例名稱。
  • type_param:測試套的附加信息。默認為無
  • value_param:測試案例的附加信息。默認為無
  • fixture_class_id:test fixture類的id
  • set_up_tc :函數指針,指向函數SetUpTestCaseFunc
  • tear_down_tc:函數指針,指向函數TearDownTestCaseFunc
  • factory:指向工廠對象的指針,該工廠對象創建上面TEST宏生成的測試類的對象

我們看到在MakeAndRegisterTestInfo函數體中定義了一個TestInfo對象,該對象包含了一個TEST宏中標識的測試案例的測試套名稱、測試案例名稱、測試套附加信息、測試案例附加信息、創建測試案例類對象的工廠對象的指針這些信息。

下面大家可能就會比較好奇所謂的工廠對象,可以在gtest-internal.h中找帶它的定義

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

TestFactoryImpl類是一個模板類,它的作用就是單純的創建對應於模板形參類型的測試案例對象。因為模板的存在也大大簡化了代碼,否則可能就要寫無數個TestFactoryImpl類了,呵呵。

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

乍一看似乎是對test_info對象的一些熟悉信息進行設置。究竟是怎么樣呢?源碼面前,了無秘密,我們還是得去找到它的源碼,在gtest-internal-inl中可以找到它的定義

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

它的實現也是非常簡單,關鍵還是在UnitTest類的成員函數GetInstance和返回類型的成員函數impl,繼續追蹤下去

class GTEST_API_ UnitTest {
public:
// Gets the singleton UnitTest object.  The first time this method
// is called, a UnitTest object is constructed and returned.
// Consecutive calls will return the same object.
  static UnitTest* GetInstance();
 
  internal::UnitTestImpl* impl() { return impl_; }
  const internal::UnitTestImpl* impl() const { return impl_; }

private:
  mutable internal::Mutex mutex_;
  internal::UnitTestImpl* impl_;
}

UnitTest * UnitTest::GetInstance() {
#if (_MSC_VER == 1310 && !defined(_DEBUG)) || defined(__BORLANDC__)
   static UnitTest* const instance = new UnitTest;
   return instance;
#else
   static UnitTest instance;
   return &instance;
}

根據代碼和注釋可知GetInstance是Unitest類的成員函數,它僅僅是生成一個靜態的UniTest對象然后返回。實際上這么做是為了實現UniTest類的單例(Singleton)實例。而impl只是單純的返回UniTest的UnitTestImpl類型的指針數據成員impl_。

再聯系之前的代碼,通過UnitTestImpl類的AddTestInfo設置Test_Info類對象的信息。其實繞了一圈,最終就是通過AddTestInfo設置Test_info類對象的信息,自然地,我們需要知道AddTestInfo的實現啦:

void AddTestInfo(Test::SetUpTestCaseFunc set_up_tc,
                   Test::TearDownTestCaseFunc tear_down_tc,
                   TestInfo* test_info) {
    GetTestCase(test_info->test_case_name(),
                test_info->type_param(),
                set_up_tc,
                tear_down_tc)->AddTestInfo(test_info);
}

而AddTestInfo是通過GetTestCase函數實現的

TestCase* UnitTestImpl::GetTestCase(const char* test_case_name,
                                    const char* type_param,
                                    Test::SetUpTestCaseFunc set_up_tc,
                                    Test::TearDownTestCaseFunc tear_down_tc) {
  // Can we find a TestCase with the given name?
  const std::vector<TestCase*>::const_iterator test_case =
      std::find_if(test_cases_.begin(), test_cases_.end(),
                   TestCaseNameIs(test_case_name));

  if (test_case != test_cases_.end())
    return *test_case;

  // No.  Let's create one.
  TestCase* const new_test_case =
      new TestCase(test_case_name, type_param, set_up_tc, tear_down_tc);

  // Is this a death test case?
  if (internal::UnitTestOptions::MatchesFilter(String(test_case_name),
                                               kDeathTestCaseFilter)) {
    // Yes.  Inserts the test case after the last death test case
    // defined so far.  This only works when the test cases haven't
    // been shuffled.  Otherwise we may end up running a death test
    // after a non-death test.
    ++last_death_test_case_;
    test_cases_.insert(test_cases_.begin() + last_death_test_case_,
                       new_test_case);
  } else {
    // No.  Appends to the end of the list.
    test_cases_.push_back(new_test_case);
  }

  test_case_indices_.push_back(static_cast<int>(test_case_indices_.size()));
  return new_test_case;
}

從上面代碼可以看出其實並不是一開始猜測的設置Test_Info對象的信息,而是判斷包含Test_info對象中的測試套名稱、測試案例名稱等信息的TestCase對象的指針是否在一個vector向量中,若存在就返回這個指針;若不存在就把創建一個包含這些信息的TestCase對象的指針加入到vector向量中,並返回這個指針。

至於vector向量test_cases_,它是UnitTestImpl中的私有數據成員,在這個向量中存放了整個測試項目中所有包含測試套、測試案例等信息的TestCase對象的指針。

緊接着我們看到從GetTestCase返回的TestCase指針調用TestCase類中的成員函數AddTestInfo,在gtest.cc中可以找到它的定義如下:

void TestCase::AddTestInfo(TestInfo * test_info) {
  test_info_list_.push_back(test_info);
  test_indices_.push_back(static_cast<int>(test_indices_.size()));
}

調用這個函數的目的是在於將Test_info對象添加到test_info_list_中,而test_info_list_是類TestCase中的私有數據成員,它也是一個vector向量。原型為

std::vector<TestInfo*> test_info_list_;

該向量保存着整個項目中所有包含測試案例對象各種信息的Test_Info對象的指針。

而test_indices_也是類TestCase中的私有數據成員,保存着test_info_list中每個元素的索引號。它仍然是一個vector向量,原型為

std::vector<int> test_indices_;

TEST宏

此時,我們再來看看TEST宏的具體定義實現:

#if !GTEST_DONT_DEFINE_TEST
# define TEST(test_case_name, test_name) GTEST_TEST(test_case_name, test_name)
#endif

#define GTEST_TEST(test_case_name, test_name)\
  GTEST_TEST_(test_case_name, test_name, \
              ::testing::Test, ::testing::internal::GetTestTypeId())

#define TEST_F(test_fixture, test_name)\
  GTEST_TEST_(test_fixture, test_name, test_fixture, \
              ::testing::internal::GetTypeId<test_fixture>())

可以看到,TEST宏和事件機制對於的TEST_F宏都是調用了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_ATTRIBUTE_UNUSED_;\
  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, NULL, NULL, \
        (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()

我們終於看到了在預處理展開中得到的案例類的定義和注冊案例類對象信息的定義代碼啦。唯一的疑問在於類的名字是GTEST_TEST_CLASS_NAME_,從字面意思可以知道這宏就是獲得類的名字

#define GTEST_TEST_CLASS_NAME_(test_case_name, test_name) \
  test_case_name##_##test_name##_Test

果不其然,宏GTEST_TEST_CLASS_NAME的功能就是把兩個參數拼接為一個參數。

RUN_ALL_TESTS宏

我們的測試程序就是從main函數中的RUN_ALL_TEST的調用開始的,在gtest.h中可以找到該宏的定義

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

RUN_ALL_TESTS就是簡單的調用UnitTest的成員函數GetInstance,我們知道GetInstance就是返回一個單例(Singleton)UnitTest對象,該對象調用成員函數Run

int UnitTest::Run() {
  impl()->set_catch_exceptions(GTEST_FLAG(catch_exceptions));

  return internal::HandleExceptionsInMethodIfSupported(
      impl(),
      &internal::UnitTestImpl::RunAllTests,
     "auxiliary test code (environments or event listeners)") ? 0 : 1;
}

Run函數也是簡單的調用HandleExceptionsInMethodIfSupported函數,追蹤它的實現

template <class T, typename Result>
Result HandleExceptionsInMethodIfSupported(
    T* object, Result (T::*method)(), const char* location) {

    if (internal::GetUnitTestImpl()->catch_exceptions()) {
     ......  //異常處理省略
     } else {
     return (object->*method)();
   }
 }

HandleExceptionsInMethodIfSupported是一個模板函數,他的模板形參具現化為調用它的UnitTestImpl和int,也就是T = UnitTestImpl, Result = int。在函數體里調用UnitTestImpl類的成員函數RunAllTests

bool UnitTestImpl::RunAllTests() {
    ......
    const TimeInMillis start = GetTimeInMillis();  //開始計時
    if (has_tests_to_run && GTEST_FLAG(shuffle)) {
       random()->Reseed(random_seed_);
       ShuffleTests();
     }
     repeater->OnTestIterationStart(*parent_, i);

     if (has_tests_to_run) {
       //初始化全局的SetUp事件
       repeater->OnEnvironmentsSetUpStart(*parent_);
       //順序遍歷注冊全局SetUp事件
       ForEach(environments_, SetUpEnvironment);
       //初始化全局TearDown事件
       repeater->OnEnvironmentsSetUpEnd(*parent_);
       // 
       // set-up.
       if (!Test::HasFatalFailure()) {
         for (int test_index = 0; test_index < total_test_case_count();
              test_index++) {
           GetMutableTestCase(test_index)->Run(); //TestCase::Run
         }
       }
      // 反向遍歷取消所有全局事件.
      repeater->OnEnvironmentsTearDownStart(*parent_);
     std::for_each(environments_.rbegin(), environments_.rend(),
                    TearDownEnvironment);
      repeater->OnEnvironmentsTearDownEnd(*parent_);
    }
    elapsed_time_ = GetTimeInMillis() - start; //停止計時
    ......
}

如上面代碼所示,UnitTestImpl::RunAllTests主要進行全局事件的初始化,以及變量注冊。而真正的執行部分在於調用GetMutableTestCase

TestCase* UnitTest::GetMutableTestCase(int i) {
  return impl()->GetMutableTestCase(i); //impl返回UnitTestImpl類型指針
}

TestCase* UnitTestImpl:: GetMutableTestCase(int i) {
    const int index = GetElementOr(test_case_indices_, i, -1);
    return index < 0 ? NULL : test_cases_[index];
}

經過兩次調用返回vector向量test_cases_中的元素,它的元素類型為TestCase類型。然后調用TestCase::Run

void TestCase::Run() {
  ......  //省略
  const internal::TimeInMillis start = internal::GetTimeInMillis();
  for (int i = 0; i < total_test_count(); i++) {
    GetMutableTestInfo(i)->Run(); //調用TestCase::GetMutableTestInfo
  }                                     //以及Test_Info::Run
  ...... //省略
}

TestInfo* TestCase::GetMutableTestInfo(int i) {
  const int index = GetElementOr(test_indices_, i, -1);
  return index < 0 ? NULL : test_info_list_[index];
}

看到又轉向調用TestCase::GetMutableTestInfo,返回向量test_info_list_的元素。而它的元素類型為Test_info。進而又轉向了Test_info::Run

void TestInfo::Run() {
  ......  //省略
  Test* const test = internal::HandleExceptionsInMethodIfSupported(
      factory_, &internal::TestFactoryBase::CreateTest,
      "the test fixture's constructor");
  ......  //省略
    test->Run();  // Test::Run
  ......   //省略
  }

在TestInfo::Run中調用了HandleExceptionsInMethodIfSupported,通過上文中的分析可以得知該函數在這個地方最終的作用是調用internal::TestFactoryBase::CreateTest將factor_所指的工廠對象創建的測試案例對象的地址賦給Test類型的指針test。所以最后調用了Test::Run。

void Test::Run() {
  if (!HasSameFixtureClass()) return;

  internal::UnitTestImpl* const impl = internal::GetUnitTestImpl();
  impl->os_stack_trace_getter()->UponLeavingGTest();
  internal::HandleExceptionsInMethodIfSupported(this, &Test::SetUp, "SetUp()");
  // We will run the test only if SetUp() was successful.
  if (!HasFatalFailure()) {
    impl->os_stack_trace_getter()->UponLeavingGTest();
    internal::HandleExceptionsInMethodIfSupported(
        this, &Test::TestBody, "the test body");
  }

  // However, we want to clean up as much as possible.  Hence we will
  // always call TearDown(), even if SetUp() or the test body has
  // failed.
  impl->os_stack_trace_getter()->UponLeavingGTest();
  internal::HandleExceptionsInMethodIfSupported(
      this, &Test::TearDown, "TearDown()");
}

在Test::Run函數體中我們看到通過HandleExceptionsInMethodIfSupported調用了TestBody,先來看看Test中TestBody的原型聲明

virtual void TestBody() = 0;

TestBody被聲明為純虛函數。一切都明朗了,在上文中通過test調用Test::Run,進而通過test::調用TestBody,而test實際上是指向繼承自Test類的案例類對象,進而發生了多態,調用的是Test_foo_test_normal_Test::TestBody,也就是我們最初在TEST或者TEST_F宏中所寫的測試代碼。

如此遍歷,就是順序執行測試demo程序中所寫的每一個TEST宏的函數體啦。

總結

經過對預處理得到的TEST宏進行逆向跟蹤,到正向跟蹤RUN_ALL_TESTS宏,了解了gtest的整個運行過程,里面涉及到一下GOF設計模式的運用,比如工廠函數、Singleton、Impl等。仔細推敲便可發現gtest設計層層跳轉,雖然有些復雜,但也非常巧妙,很多地方非常值得我們自己寫代碼的時候學習的。

另外本文沒有提到的地方如斷言宏,輸出log日志等,因為比較簡單就略過了。斷言宏和輸出log就是在每次遍歷調用TestBody的時候進行相應的判斷和輸出打印,有興趣的童鞋可以自行研究啦。

下圖是一個簡單的TEST宏展開后的流程圖

  

最后再簡單將gtest的運行過程簡述一遍:

  1. 整個測試項目只有一個UnitTest對象,因而整個項目也只有一個UnitTestImpl對象。
  2. 每一個TEST宏生成一個測試案例類,繼承自Test類。
  3. 對於每一個測試案例類,由一個工廠類對象創建該類對象。
  4. 由該測試案例類對象創建一個Test_Info類對象。
  5. 由Test_Info類對象創建一個Test_case對象
  6. 創建Test_case對象的指針,並將其插入到UnitTestImpl對象的數據成員vector向量的末尾位置。
  7. 對每一個TEST宏進行2-6步驟,那么對於唯一一個UnitTestImpl對象來說,它的數據成員vector向量中的元素按順序依次指向每一個包含測試案例對象信息的TestCase對象。
  8. 執行RUN_ALL_TESTS宏,開始執行用例。從頭往后依次遍歷UnitTestImpl對象中vector向量的中的元素,對於其中的每一個元素指針,經過一系列間接的方式最終調用其所對應的測試案例對象的TestBody成員函數,即測試用例代碼。

參考文獻

  1. FAQ - Google Test

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM