1、前言
Google test是一款開源的白盒單元測試架構,據說目前在Google内部已在幾千個項目中應用了基于該架構的白盒測試。
最近的工作是在搞一個基于gtest架構搭建的自動化白盒測試項目,該項目上線也有一段時間了,目前來說效果還是挺不錯的。
侯捷先生在《STL源碼剖析》中說過一句話:”會用STL,是一種檔次。對STL原理有所了解,又是一個檔次。追蹤過STL源碼又是一個檔次。第三種檔次的人用起STL來,虎虎生風之勢絕非第一檔次的人能夠望其項背。“
我認為使用一種架構時也是一樣,隻有當你知道架構内部是如何運作的,不僅知其然,還知其是以然,才能避免一些坑,使測試架構用起來更效率。
就拿平常項目中用的最簡單的一個測試demo(test_foo.cpp)來說吧
1 int foo(int a, int b)
2 {
3 return a + b;
4 }
5
6 class TestWidget : public testing::Environment
7 {
8 public:
9 virtual void SetUp();
10 virtual void TearDown();
11 };
12
13 TEST(Test_foo, test_normal)
14 {
15 EXPECT_EQ(2, foo(1, 1));
16 }
17
18 int main(int argc, char const *argv[])
19 {
20 testing::AddGlobalTestEnvironment(new TestSysdbg);
21 testing::InitGoogleTest(&argc, argv);
22 return RUN_ALL_TESTS();
23 return 0;
24 }
你可知道gtest是如何調用被測接口,如何輸出測試結果的嗎?本文主要解答這個問題。
2、解析
2.1、從預處理開始
需要提到的是Gtest中用到了許多的宏技巧以及c++的模闆元技巧。
先不看源碼中TEST宏的定義,直接用下面指令單獨調用預處理器對源檔案進行預處理:
cpp test_foo.cpp test_foo.i –I/ gtest/gtest-1.6/
打開生成的經過預處理的檔案test_foo.i
1 class Test_foo_test_normal_Test : public ::testing::Test
2 {
3 public:
4 Test_foo_test_normal_Test() {}
5
6 private:
7 virtual void TestBody();
8 public:
9 virtual void SetUp();
10 virtual void TearDown();
11 };
12
13 class Test_foo_test_normal_Test : public ::testing::Test
14 {
15 public:
16 Test_foo_test_normal_Test() {}
17
18 private:
19 virtual void TestBody();
20 static ::testing::TestInfo* const test_info_ __attribute__ ((unused));
21 Test_foo_test_normal_Test(Test_foo_test_normal_Test const &);
22 void operator=(Test_foo_test_normal_Test const &);
23 };
24
25 ::testing::TestInfo* const Test_foo_test_normal_Test
26 ::test_info_ =
27 ::testing::internal::MakeAndRegisterTestInfo(
28 "Test_foo", "test_normal", __null, __null,
29 (::testing::internal::GetTestTypeId()),
30 ::testing::Test::SetUpTestCase,
31 ::testing::Test::TearDownTestCase,
32 new ::testing::internal::TestFactoryImpl<Test_foo_test_normal_Test>);
33
34 void Test_foo_test_normal_Test::TestBody()
35 {
36 switch (0)
37 case 0:
38 default:
39 if (const ::testing::AssertionResult gtest_ar =
40 (::testing::internal::
41 EqHelper<(sizeof(::testing::internal::IsNullLiteralHelper(2)) == 1) >
42 ::Compare("2", "foo(1, 1)", 2, foo(1, 1)))) ;
43 else
44 ::testing::internal::AssertHelper(::testing::TestPartResult::kNonFatalFailure,
45 "test_foo.cpp", 17, gtest_ar.failure_message()) = ::testing::Message();
46 }
47
48 int main(int argc, char *argv[])
49 {
50 testing::AddGlobalTestEnvironment(new TestWidget);
51 testing::InitGoogleTest(&argc, argv);
52 return (::testing::UnitTest::GetInstance()->Run());
53 return 0;
54 }
我們可以看到TEST宏經過預處理器處理後展開為:
1、定義了一個繼承自::testing::test類的新類Test_foo_test_normal_Test,該類的名字為TEST宏兩個形參的拼接而成。
2、TEST宏中的測試代碼被展開并定義為生成類的成員函數TestBody的函數體。
3、生成類的靜态資料成員test_info_被初始化為函MakeAndRegisterTestInfo的傳回值。具體意義後面介紹。
2.2、MakeAndRegisterTestInfo函數
從上面來看MakeAndRegisterTestInfo函數是一個比較關鍵的函數了,從字面意思上看就是生成并注冊該測試案例的資訊,在頭檔案gtest.cc中可以找到關于它的定義,他是一個testing命名空間中的嵌套命名空間internal中的非成員函數:
1 TestInfo* MakeAndRegisterTestInfo(
2 const char* test_case_name, const char* name,
3 const char* type_param,
4 const char* value_param,
5 TypeId fixture_class_id,
6 SetUpTestCaseFunc set_up_tc,
7 TearDownTestCaseFunc tear_down_tc,
8 TestFactoryBase* factory) {
9 TestInfo* const test_info =
10 new TestInfo(test_case_name, name, type_param, value_param,
11 fixture_class_id, factory);
12 GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
13 return test_info;
14 }
其中形參的意義如下:
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中找帶它的定義
1 template <class TestClass>
2 class TestFactoryImpl : public TestFactoryBase {
3 public:
4 virtual Test* CreateTest() { return new TestClass; }
5 };
TestFactoryImpl類是一個模闆類,它的作用就是單純的生産對應于模闆形參類型的測試案例對象。因為模闆的存在也大大簡化了代碼,否則可能就要寫無數個TestFactoryImpl類了,呵呵。
1 GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
乍一看似乎是對test_info對象的一些熟悉資訊進行設定。究竟是怎麼樣呢?源碼面前,了無秘密,我們還是得去找到它的源碼,在gtest-internal-inl中可以找到它的定義
1 inline UnitTestImpl* GetUnitTestImpl() {
2 return UnitTest::GetInstance()->impl();
3 }
可以看到它的實作也是非常簡單,關鍵還是在UnitTest類的成員函數GetInstance和傳回類型的成員函數impl,我們繼續追蹤下去
1 class GTEST_API_ UnitTest {
2 public:
3 // Gets the singleton UnitTest object. The first time this method
4 // is called, a UnitTest object is constructed and returned.
5 // Consecutive calls will return the same object.
6 static UnitTest* GetInstance();
7
8 internal::UnitTestImpl* impl() { return impl_; }
9 const internal::UnitTestImpl* impl() const { return impl_; }
10
11 private:
12 mutable internal::Mutex mutex_;
13 internal::UnitTestImpl* impl_;
14 }
15
16 UnitTest * UnitTest::GetInstance() {
17 #if (_MSC_VER == 1310 && !defined(_DEBUG)) || defined(__BORLANDC__)
18 static UnitTest* const instance = new UnitTest;
19 return instance;
20 #else
21 static UnitTest instance;
22 return &instance;
23 }
根據代碼和注釋可知GetInstance是Unitest類的成員函數,它僅僅是生成一個靜态的UniTest對象然後傳回。實際上這麼做是為了實作UniTest類的單例(Singleton)執行個體。而impl隻是單純的傳回UniTest的UnitTestImpl類型的指針資料成員impl_。
再聯系之前的代碼,通過UnitTestImpl類的AddTestInfo設定Test_Info類對象的資訊。其實繞了一圈,最終就是通過AddTestInfo設定Test_info類對象的資訊,自然地,我們需要知道AddTestInfo的實作啦:
1 void AddTestInfo(Test::SetUpTestCaseFunc set_up_tc,
2 Test::TearDownTestCaseFunc tear_down_tc,
3 TestInfo* test_info) {
4 GetTestCase(test_info->test_case_name(),
5 test_info->type_param(),
6 set_up_tc,
7 tear_down_tc)->AddTestInfo(test_info);
8 }
我們看到是通過GetTestCase函數實作的
1 TestCase* UnitTestImpl::GetTestCase(const char* test_case_name,
2 const char* type_param,
3 Test::SetUpTestCaseFunc set_up_tc,
4 Test::TearDownTestCaseFunc tear_down_tc) {
5 // Can we find a TestCase with the given name?
6 const std::vector<TestCase*>::const_iterator test_case =
7 std::find_if(test_cases_.begin(), test_cases_.end(),
8 TestCaseNameIs(test_case_name));
9
10 if (test_case != test_cases_.end())
11 return *test_case;
12
13 // No. Let's create one.
14 TestCase* const new_test_case =
15 new TestCase(test_case_name, type_param, set_up_tc, tear_down_tc);
16
17 // Is this a death test case?
18 if (internal::UnitTestOptions::MatchesFilter(String(test_case_name),
19 kDeathTestCaseFilter)) {
20 // Yes. Inserts the test case after the last death test case
21 // defined so far. This only works when the test cases haven't
22 // been shuffled. Otherwise we may end up running a death test
23 // after a non-death test.
24 ++last_death_test_case_;
25 test_cases_.insert(test_cases_.begin() + last_death_test_case_,
26 new_test_case);
27 } else {
28 // No. Appends to the end of the list.
29 test_cases_.push_back(new_test_case);
30 }
31
32 test_case_indices_.push_back(static_cast<int>(test_case_indices_.size()));
33 return new_test_case;
34 }
我們看到其實并不是一開始猜測的設定Test_Info對象的資訊,而是判斷包含Test_info對象中的測試套名稱、測試案例名稱等資訊的TestCase對象的指針是否在一個vector向量中,若存在就傳回這個指針;若不存在就把建立一個包含這些資訊的TestCase對象的指針加入到vector向量中,并傳回這個指針。
至于vector向量test_cases_是UnitTestImpl中的私有資料成員,在這個向量中存放了整個測試項目中所有包含測試套、測試案例等資訊的TestCase對象的指針。
緊接着我們看到從GetTestCase傳回的TestCase指針調用TestCase類中的成員函數AddTestInfo,我們可以找到它的定義如下:
1 void TestCase::AddTestInfo(TestInfo * test_info) {
2 test_info_list_.push_back(test_info);
3 test_indices_.push_back(static_cast<int>(test_indices_.size()));
4 }
我們看到,調用這個函數的目的是在于将Test_info對象添加到test_info_list_中,而test_info_list_是類TestCase中的私有資料成員,它也是一個vector向量。原型為
1 std::vector<TestInfo*> test_info_list_;
該向量儲存着整個項目中所有包含測試案例對象各種資訊的Test_Info對象的指針。
而test_indices_也是類TestCase中的私有資料成員,儲存着test_info_list中每個元素的索引号。它仍然是一個vector向量,原型為
1 std::vector<int> test_indices_;
2.3、TEST宏
此時,我們再來看看TEST宏的具體定義實作:
1 #if !GTEST_DONT_DEFINE_TEST
2 # define TEST(test_case_name, test_name) GTEST_TEST(test_case_name, test_name)
3 #endif
4
5 #define GTEST_TEST(test_case_name, test_name)\
6 GTEST_TEST_(test_case_name, test_name, \
7 ::testing::Test, ::testing::internal::GetTestTypeId())
8
9 #define TEST_F(test_fixture, test_name)\
10 GTEST_TEST_(test_fixture, test_name, test_fixture, \
11 ::testing::internal::GetTypeId<test_fixture>())
可以看到,TEST宏和事件機制對于的TEST_F宏都是調用了GTEST_TEST_宏,我們再追蹤這個宏的定義
1 #define GTEST_TEST_(test_case_name, test_name, parent_class, parent_id)\
2 class GTEST_TEST_CLASS_NAME_(test_case_name, test_name) : public parent_class {\
3 public:\
4 GTEST_TEST_CLASS_NAME_(test_case_name, test_name)() {}\
5 private:\
6 virtual void TestBody();\
7 static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;\
8 GTEST_DISALLOW_COPY_AND_ASSIGN_(\
9 GTEST_TEST_CLASS_NAME_(test_case_name, test_name));\
10 };\
11 \
12 ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)\
13 ::test_info_ =\
14 ::testing::internal::MakeAndRegisterTestInfo(\
15 #test_case_name, #test_name, NULL, NULL, \
16 (parent_id), \
17 parent_class::SetUpTestCase, \
18 parent_class::TearDownTestCase, \
19 new ::testing::internal::TestFactoryImpl<\
20 GTEST_TEST_CLASS_NAME_(test_case_name, test_name)>);\
21 void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestBody()
我們終于看到了在預處理展開中得到的案例類的定義和注冊案例類對象資訊的定義代碼啦。唯一的疑問在于類的名字是GTEST_TEST_CLASS_NAME_,從字面意思可以指定這宏就是獲得類的名字
1 #define GTEST_TEST_CLASS_NAME_(test_case_name, test_name) \
2 test_case_name##_##test_name##_Test
可以看到宏GTEST_TEST_CLASS_NAME的功能就是把兩個參數拼接為一個參數。
2.4、RUN_ALL_TESTS宏
我們的測試程式就是從main函數中的RUN_ALL_TEST的調用開始的,在gtest.h中可以找到該宏的定義
1 #define RUN_ALL_TESTS()\
2 (::testing::UnitTest::GetInstance()->Run())
RUN_ALL_TESTS就是簡單的調用UnitTest的成員函數GetInstance,我們知道GetInstance就是傳回一個單例(Singleton)UnitTest對象,該對象調用成員函數Run
1 int UnitTest::Run() {
2 impl()->set_catch_exceptions(GTEST_FLAG(catch_exceptions));
3
4 return internal::HandleExceptionsInMethodIfSupported(
5 impl(),
6 &internal::UnitTestImpl::RunAllTests,
7 "auxiliary test code (environments or event listeners)") ? 0 : 1;
8 }
Run函數也是簡單的調用HandleExceptionsInMethodIfSupported函數,追蹤它的實作
1 template <class T, typename Result>
2 Result HandleExceptionsInMethodIfSupported(
3 T* object, Result (T::*method)(), const char* location) {
4
5 if (internal::GetUnitTestImpl()->catch_exceptions()) {
6 ...... //異常處理省略
7 } else {
8 return (object->*method)();
9 }
10 }
我們看到HandleExceptionsInMethodIfSupported是一個模闆函數,他的模闆形參具現化為調用它的UnitTestImpl和int,也就是T = UnitTestImpl, Result = int。在函數體裡調用UnitTestImpl類的成員函數RunAllTests
1 bool UnitTestImpl::RunAllTests() {
2 ......
3 const TimeInMillis start = GetTimeInMillis(); //開始計時
4 if (has_tests_to_run && GTEST_FLAG(shuffle)) {
5 random()->Reseed(random_seed_);
6 ShuffleTests();
7 }
8 repeater->OnTestIterationStart(*parent_, i);
9
10 if (has_tests_to_run) {
11 //初始化全局的SetUp事件
12 repeater->OnEnvironmentsSetUpStart(*parent_);
13 //順序周遊注冊全局SetUp事件
14 ForEach(environments_, SetUpEnvironment);
15 //初始化全局TearDown事件
16 repeater->OnEnvironmentsSetUpEnd(*parent_);
17 //
18 // set-up.
19 if (!Test::HasFatalFailure()) {
20 for (int test_index = 0; test_index < total_test_case_count();
21 test_index++) {
22 GetMutableTestCase(test_index)->Run(); //TestCase::Run
23 }
24 }
25 // 反向周遊取消所有全局事件.
26 repeater->OnEnvironmentsTearDownStart(*parent_);
27 std::for_each(environments_.rbegin(), environments_.rend(),
28 TearDownEnvironment);
29 repeater->OnEnvironmentsTearDownEnd(*parent_);
30 }
31 elapsed_time_ = GetTimeInMillis() - start; //停止計時
32 ......
33 }
如上面代碼所示,UnitTestImpl::RunAllTests主要進行全局事件的初始化,以及變量注冊。而真正的執行部分在于調用GetMutableTestCase
1 TestCase* UnitTest::GetMutableTestCase(int i) {
2 return impl()->GetMutableTestCase(i); //impl傳回UnitTestImpl類型指針
3 }
4
5 TestCase* UnitTestImpl:: GetMutableTestCase(int i) {
6 const int index = GetElementOr(test_case_indices_, i, -1);
7 return index < 0 ? NULL : test_cases_[index];
8 }
經過兩次調用傳回vector向量test_cases_中的元素,它的元素類型為TestCase類型。然後調用TestCase::Run
1 void TestCase::Run() {
2 ...... //省略
3 const internal::TimeInMillis start = internal::GetTimeInMillis();
4 for (int i = 0; i < total_test_count(); i++) {
5 GetMutableTestInfo(i)->Run(); //調用TestCase::GetMutableTestInfo
6 } //以及Test_Info::Run
7 ...... //省略
8 }
9
10 TestInfo* TestCase::GetMutableTestInfo(int i) {
11 const int index = GetElementOr(test_indices_, i, -1);
12 return index < 0 ? NULL : test_info_list_[index];
13 }
看到又轉向調用TestCase::GetMutableTestInfo,傳回向量test_info_list_的元素。而它的元素類型為Test_info。進而又轉向了Test_info::Run
1 void TestInfo::Run() {
2 ...... //省略
3 Test* const test = internal::HandleExceptionsInMethodIfSupported(
4 factory_, &internal::TestFactoryBase::CreateTest,
5 "the test fixture's constructor");
6 ...... //省略
7 test->Run(); // Test::Run
8 ...... //省略
9 }
可以看到在TestInfo::Run中調用了HandleExceptionsInMethodIfSupported,通過上文中的分析可以得知該函數在這個地方最終的作用是調用internal::TestFactoryBase::CreateTest将factor_所指的工廠對象建立的測試案例對象的位址賦給Test類型的指針test。所有最後調用了Test::Run。
1 void Test::Run() {
2 if (!HasSameFixtureClass()) return;
3
4 internal::UnitTestImpl* const impl = internal::GetUnitTestImpl();
5 impl->os_stack_trace_getter()->UponLeavingGTest();
6 internal::HandleExceptionsInMethodIfSupported(this, &Test::SetUp, "SetUp()");
7 // We will run the test only if SetUp() was successful.
8 if (!HasFatalFailure()) {
9 impl->os_stack_trace_getter()->UponLeavingGTest();
10 internal::HandleExceptionsInMethodIfSupported(
11 this, &Test::TestBody, "the test body");
12 }
13
14 // However, we want to clean up as much as possible. Hence we will
15 // always call TearDown(), even if SetUp() or the test body has
16 // failed.
17 impl->os_stack_trace_getter()->UponLeavingGTest();
18 internal::HandleExceptionsInMethodIfSupported(
19 this, &Test::TearDown, "TearDown()");
20 }
在Test::Run函數體中我們看到通過HandleExceptionsInMethodIfSupported調用TestBody,我們先來看看Test中TestBody的原型聲明
1 virtual void TestBody() = 0;
TestBody被聲明為純虛函數。一切都明朗了,在上文中通過test調用Test::Run,進而通過test::調用TestBody,而test實際上是指向繼承自Test類的案例類對象,進而發生了多态,調用的是Test_foo_test_normal_Test::TestBody,也就是我們最初在TEST或者TEST_F宏中所寫的測試代碼。
如此周遊,就是順序執行我們所寫的每一個TEST宏的函數體啦。
3、總結
經過對預處理得到的TEST宏進行逆向跟蹤,到正向跟蹤RUN_ALL_TESTS宏,了解了gtest的整個運作過程,裡面涉及到一下GOF設計模式的運用,比如工廠函數、Singleton、Impl等。仔細推敲便可發現gtest設計層層跳轉,雖然有些複雜,但也非常巧妙,很多地方非常值得我們自己寫代碼的時候學習的。
另外本文沒有提到的地方如斷言宏,輸出log日志等,因為比較簡單就略過了。斷言宏和輸出log就是在每次周遊調用TestBody的時候進行相應的判斷和輸出列印,有興趣的童鞋可以自行研究啦。
最後再簡單将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成員函數,即測試用例代碼。
(完)
分類: Unittest