Gtest/Gmock探究(二)--TEST宏分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/songqier/article/details/78822325

    刚开始看Gtest/Gmock使用方法的时候,自己写了一些测试代码,能工作,但是总觉得有些抽象。你可能会跟我一样有如下疑问:
    • 为什么写了TEST宏,我们自定义的测试就能被运行
    • 为什么MOCK_METHODX系列宏只需要用来声明函数就行了,我们该怎样定义这些被mock的函数的函数体具体逻辑呢?
    • 有些语法一不注意就会写错,比如下面的代码:

EXPECT_CALL(*pManager, findAccountForUser(testing::_))
        .Times(2)
        .WillRepeatedly(testing::Invoke(&helper, &AccountHelper::findAccountForUser));

    容易在.Time(2)后面加逗号等等。
    我们将带着上面的问题开始分析gtest源码。我相信在分析结束后,上述的问题会有相应的答案,并且到时你再去看gtest/gmock相关的说明文档时,也不会再那么抽象了。在文档中,我会介绍一些相关的分析和调试的工具以及使用方法。

    好,下面开始解开gtest中TEST宏的奥秘:
    我们先写一个非常简单的测试用例,该用例用来测试C函数库里strcmp函数的功能。代码如下:

// TEST_macro_analysis.cpp
#include "gtest/gtest.h"
#include "gmock/gmock.h"
#include <string.h>

TEST(StringMothodTest, strcmp) {
    const char *cmp0 = "hello";
    const char *cmp1 = "hello";
    EXPECT_EQ(strcmp(cmp0, cmp1), 0);
}

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

    这段代码完成了一个TEST宏(实际就是一个test case),根据gtest说明文档知道,这个宏第一个参数为test case name,第二个参数为test name。Gtest采用了自己定义的专有名词规则,一个test case可以包含多个test。关于这部分的详细介绍,可以参考这个页面的第二小节“Beware of the nomenclature”。
    上面示例代码中,我们在TEST宏里自定义的测试代码逻辑为:调用strcmp函数比较cmp0和cmp1两个字符串,并使用EXPECT_EQ宏来期望strcmp函数返回的值为0.
    在main函数中,按照gtest规范,先调用::testing::InitGoogleTest函数初始化gtest,然后调用RUN_ALL_TESTS()函数开始执行单元测试。

    代码文件路径结构
    这里写图片描述
    (源码文件名:Test_macro_analysis.cpp,在文件夹gtest_TEST下,这个文件夹跟gmock-1.7.0在同一级)
    之所以要给出代码目录结构,是为了后面编译的时候需要设置头文件索引路径,到时大家才知道我为啥要那样设置索引路径。
    1. 首先编译非优化的gtest。
        A. 进入gmock.1.7.0目录,运行./configure。(点击这里下载gmock1.7.0
          这个命令会生成gmock工程的Makefile
        B. 去掉Makefile文件中的”-O”优化参数字段(为了我们调试时能跟进并精准定位到gtest源代码)
          有两个Makefile需要修改:gmock-1.7.0/Makefile和:gmock-1.7.0/gtest/Makefile
          使用vi打开Makefile,搜索“-O”(使用/-O回车进行搜索),注意O是大写的O,将搜索到的两处-O2直接删除,最后保存退出(两个Makefile的修改方法是一样的)。
        C. 在gmock-1.7.0目录下执行make
    2. 编译我们的测试代码:
      cd到gtest_TEST目录,然后执行:

g++ -g TEST_macro_analysis.cpp -o test -lpthread -lc -lm -lrt -lgtest -lgmock -L ../gmock-1.7.0/lib/.libs -L ../gmock-1.7.0/gtest/lib/.libs -I../gmock-1.7.0/include -I../gmock-1.7.0/gtest/include

    3. 运行和输出:
        首先设置gtest/gmock的库索引路径:

export LD_LIBRARY_PATH=../gmock-1.7.0/lib/.libs:../gmock-1.7.0/gtest/lib/.libs:$LD_LIBRARY_PATH

        运行可执行测试程序:
    这里写图片描述

    代码逻辑很简单,下面我们来看看这段简单的代码,gtest对它做了一些什么操作。我们先将这段代码的宏展开,分析一下静态的代码。然后使用gdb跟进程序的运行,看看gtest究竟干了些什么。

一、 宏展开
    大家知道C/C++中的宏实质就是代码替换,而且是在编译之前进行的代码替换。
    宏展开的方法有很多,最直接的方法就是程序员查看代码中宏的定义,然后打开一个notepad++,手动进行代码替换。老实话,我一开始也尝试了手动将宏展开,但当我层层替换了四五层以后就放弃了,因为在gtest源码中,很大部分功能实现都依靠了宏,层层替换展开后,代码会越来越复杂,手动展开就显得越来越吃力。
    使用编译器命令来进行宏展开,命令为:

cpp -E TEST_macro_analysis.cpp -I../gmock-1.7.0/include -I../gmock-1.7.0/gtest/include > macro_expansion.cpp

    cpp的-E参数:”Preprocess only; do not compile, assemble or link”,意思是仅仅预处理,不会编译,汇编或者链接。
    然后我们用notepad++打开macro_expansion.cpp,然后搜索上面单元测试代码中的test case名“StringMothodTest”,如果你的notepad++没有设置自动换行,那么你会看到搜索定位到这样的代码:
    这里写图片描述

Note: 为了尽可能多的展示源码信息,我截的图都比较大,但是CSDN博客正文内容宽度有限,所以一些截图被缩放,看不清细节。这时就需要各位看官单独打开这个图片链接,或者下载到本机看了。

    这个类的类名就是用我们定义的tesat case name,test name再加上_Test后缀拼接而成的。我们美化一下这段代码,得到如下的类定义代码:
这里写图片描述

Note Again: 截图图片太大,显示有缩放,请右键打开图片链接或者下载图片后查看图片细节。

    将这部分代码跟我们最开始的源代码做对比,不难看出上面截图红色框框内的代码就是我的测试代码中TEST宏展开后的代码。为了进一步证明这个观点,使用代码IDE工具(我使用的是SlickEdit,一个类似于Visual Studio或者Source Insight的IDE)跳转进入TEST宏的源码定义。注意,跳转的时候你会发现gtest源码中有不止一个TEST定义,在我们的代码中使用了#include “gtest/gtest.h”,这帮助我们定位我们使用的TEST宏定义所在的地方(gtest.h头文件中)。
这里写图片描述
再跳转进入GTEST_TEST_的定义
这里写图片描述
    截图中GTEST_TEST_CLASS_NAME宏会将test_case_nmae和test_name进行拼接并加上_Test后缀。从这里就很容易证实了图二中红色框框里的代码正式TEST宏展开后生产的。
    上图中有一个宏使用方法的技巧:最后TestBody()后面没有任何东西,这就是说,我们在TEST宏后面附加的{}大括号里的内容,实质上就变成了这个类TestBody函数的函数体。后面我们会看到测试用例开始运行时,调用的正是TestBody这个函数,而执行的函数体正是我们在TEST宏后面附加的大括号里面的内容。
    OK,这一节最后再对宏展开做个小结:

  1. 生成了以测试用例名,测试名命名加_Test后缀命名的一个类,这个类继承自gtest的::testing::Test类。
  2. 生成了全局初始化类中static成员变量的代码
  3. 最后保留TestBody函数头,这个巧妙的用法可以将我们在Test宏后面添加的大括号里的内容替换成TestBody的函数体

二、 Gdb调试跟进
    跟进调试的目的是为了了解gtest的TEST机制是怎样的逻辑,它做了哪些操作,最后是怎样执行到我们定义的TEST测试代码的。
    首先,前面我们进行了代码宏展开,里面有一个static变量的初始化代码。C++的知识告诉我们这个初始化会在main函数之前执行,所以我们先在这里打断点,看看这个初始化究竟干了什么。
    具体操作和分析如下图:
    这里写图片描述
    1. 使用gdb运行程序
    2. 设置函数断点:b ::testing::internal::MakeAndRegisterTestInfo
    3. 输入r回车运行到断点处,使用bt命令打出函数调用栈来判断是否是我们想断的地方。
    此时,键盘按ctrl + X + A,会进入gdb实时显示代码的模式。这个快捷键按完以后,界面如下:
    这里写图片描述
    可以看到MakeAndRegisterTestInfo函数里首先构造一个TestInfo对象,然后调用GetUnitTestImpl()->AddTestInfo,根据函数名大致可以猜测是记录下这个TestInfo。
    1. 最后一个factory参数:从宏展开的代码可以看到这个参数的值为new ::testing::internal::TestFactoryImpl< StringMothodTest_strcmp_Test>。这个TestFactoryImpl类实现如下:

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

    如果你有C++设计模式的一些认知,就会很容易看出这是一个工厂模式,即在这之后,调用factory->CreateTest便能new出一个StringMothodTest_strcmp_Test类的实例来。
    2. TestInfo记录了test case name,test name以及上述的factory。其余参数,我们暂时忽略。
    3. 跟进AddTestInfo函数
    首先GetUnitTestImpl函数会获取出一个static的UnitTest实例(这是设计模式中的单例模式)的私有成员变量internal::UnitTestImpl* impl_,不难猜想这个UnitTest实例就是实际控制单元测试的最顶层的类,而具体的实现是由成员变量impl_完成的。
    这里写图片描述
    然后进入GetUnitTestImpl()->AddTestInfo函数,如下:
    这里写图片描述
    从上图中也可以看到这个函数也很简单,首先设置了一个原始工作路径original_working_dir_,然后调用:

GetTestCase(test_info->test_case_name(),
            test_info->type_param(),
            set_up_tc,
            tear_down_tc)->AddTestInfo(test_info);

    接下来首先执行是GetTestCase,代码如下:
这里写图片描述
    也就是说GetTestCase会取出当前test case name的TestCase实例,如果不存在则分配一个新的并记录它。
    然后调用GetTestCase(…)->AddTestInfo(test_info),实际上调用的是TestCase::AddTestInfo
    这里写图片描述
    记录了test_info,并且也有一个indices的变量在记录每次的增长(稍后分析其作用)
    至此,全局静态初始化代码”::testing::TestInfo* const StringMothodTest_strcmp_Test ::test_info_”运行结束。小结一下,在这个初始化过程中,会为当前TEST构造一个TestCase,并且将test_Info记录到这个TestCase中,test_Info中记录了test case name,test name,当前这个TEST的类factory,以及另外三个参数信息(我们暂时不care这仨)。
    这里可以做出一个猜想:程序进入main以后,应该就是从那个全局static UnitTest里遍历所有TestCase,然后执行每个TestCase中的test_info,因为有一个factory在其中,所以很容易new出每个test_info对应的test类,然后执行类的TestBody函数。这样就把所有test case执行完了。
    我们带着这个猜想,继续往下执行。
这里写图片描述
    新设置一个main函数的断点,然后c继续运行,再一步步s命令(step)进入到RUN_ALL_TESTS宏。这里可以看到实际确实是那个UnitTest单例在控制Run,
    UnitTest::Run的代码有一点杂,我这里只关心代码是怎么Run到我们自己定义的test case里的,其余代码暂时忽略。
    这里大致介绍一下Run的代码:
这里写图片描述

    继续进入(s命令)

这里写图片描述

    就这样,调入了全局单例的函数:UnitTest::UnitTestImpl::RunAllTests(),这个函数里有一段核心代码如下:

for (int test_index = 0; test_index < otal_test_case_count(); test_index++) {
    GetMutableTestCase(test_index)->Run();
}

    其中GetMutableTestCase会从test_cases_成员变量中取出一个TestCase指针,然后开始运行TestCase::Run。
    TestCase::Run中也有一段类似的核心代码:

for (int i = 0; i < total_test_count(); i++) {
    GetMutableTestInfo(i)->Run();
}

    这段代码遍历取出每个testInfo,然后调用testInfo::Run。
    在TestInfo::Run函数中,会创建一个Test,然后调用Test::Run函数

// Creates the test object.
Test* const test = internal::HandleExceptionsInMethodIfSupported(
      factory_, &internal::TestFactoryBase::CreateTest,
      "the test fixture's constructor");

// Runs the test only if the test object was created and its
// constructor didn't generate a fatal failure.
if ((test != NULL) && !Test::HasFatalFailure()) {
    // This doesn't throw as all user code that can throw are wrapped into
    // exception handling code.
    test->Run();
}

    实际上这里创建的test就是TEST宏展开后我们的那个类的实例了,因为我们那个类是继承自Test类的,并且internal::HandleExceptionsInMethodIfSupported函数具体执行的就是factory_->CreateTest函数。
    最后再Test::Run函数中,如下代码:

internal::HandleExceptionsInMethodIfSupported(this, &Test::TestBody, "the test body");

    这个函数里最终会执行arg0->*arg1,即执行:this->TestBody,最终进入到我们TEST宏定义的代码中去
这里写图片描述
    自此,TEST宏全部分析完毕,暂时抛开诸如异常捕捉等逻辑,TEST宏可以简单小结为:
    1. 生成一个该测试用例的类
    2. 全局初始化static变量,将测试用例类信息记录到全局变量中
    3. RUN_ALL_TESTS宏会遍历全局记录的测试信息,生成对应test case类的对象,然后调用它基类的Run函数,最终调用TestBody函数。
    上述三个步骤利用了C++的static变量特性,工厂和单例设计模式,实现了自动注册测试用例,并管理控制测试用例的运行。

没有更多推荐了,返回首页