GTest Google的一种白盒单元测试框架 开源项目

  GTest为google开源的白盒单元测试跨平台测试框架,含丰富的断言、类型参数化测试、死亡测试、以及其他的测试选项设置、文件保存等,以下将对该项目C++的实现进行简要的分析,作为学习记录备份。

  基本上内部使用了大量的宏、模板,因此在分析源码时跟踪会比较麻烦,这也是有的开发项目团队不推荐使用宏、模板等,但即使如此,宏与模板的强大功能仍然是学习和开发紧凑的源码的有力武器,此外GTest也使用了C++语言和VC编译器的某些特性(类的静态成员、全局变量的初始化)(令人有点儿XXX的感觉)。

  在分析源码前,可参考GTest手册实现一个最简单的测试用例,此外以控制台程序作为分析出发点,先针对程序的入口代码,示例:

#include "gtest/gtest.h"

int _tmain(int argc, _TCHAR* argv[])

{

//    testing::GTEST_FLAG(output) = "xml:";

//    testing::GTEST_FLAG(catch_exceptions) = true;

    testing::GTEST_FLAG(repeat) = 2;

    testing::InitGoogleTest(&argc, argv);

    return RUN_ALL_TESTS();

}

GTEST_FLAG(XXX):系列的宏作为命令行或环境变量的标识,在gtest-port.h文件中有对该宏的引用,具体声明类型实际上为gtest.h文件中的GTEST_DECLARE_bool_、GTEST_DECLARE_int32_、GTEST_DECLARE_string_这三个类型,可用的声明标识如also_run_disabled_tests、break_on_failure等约15个,具体标识含义见注释说明,这些值初始值为gtest.cc中的GTEST_DEFINE_bool_、GTEST_DEFINE_int32_、GTEST_DEFINE_string_,在设置初始化值时先通过环境变量获取该值否则使用默认值(因为全局变量在进入main前CRT运行时库的_initterm中先进行初始化,此外main中也可以重新修改这些标识对应的值或者通过命令行传参修改);

InitGoogleTest:

  含重载版本,在RUN_ALL_TESTS前初始化gtest,内部主要获取命令行参数重新设置标识值,此外在深度测试时保存参数信息、注册参数化测试RegisterParameterizedTests、配置xml文件保存路径格式ConfigureXmlOutput (目前仅支持XML文件保存测试结果信息);

注册参数化测试:

  内部将需要被测试的用例进行注册,在我们编写测试用例的时候是如何知道加入测试容器的呢?跟踪宏TEST、GTEST_TEST、TEST_、TEST_P、GTEST_TEST_等可以发现实际上是将各个测试用例的左参数与右参数结合的字符串(形如test_case_name##_##test_name##_Test)当作新的类来处理,进而利用该类的静态成员变量和函数(如gtest_registering_dummy_、test_info_等静态变量)实现在进入main之前就创建该类测试用例对象并将该测试用例加入到测试用例下的容器中,此后的RUN_ALL_TESTS调用将遍历测试用例容器;

RUN_ALL_TESTS:执行所有的测试用例,内部通过UnitTest单例类的Run函数执行所有测试用例;接下来我们进入Run函数内部,详细分析是如何执行所有用例的;

执行UnitTest之Run函数:先设置异常发生时处理模式、表现方式,此后通过执行UnitTestImpl::RunAllTests函数遍历个测试用例(事实上是遍历test_cases_容器及其下的容器test_info_list_中所有TestInfo对象);

以下针对注册测试、断言宏、全局环境、参数化、死亡测试、运行参数与xml文件保存进行较为详细的分析;

注册测试及初始化前操作:初始化前操作将用户通过宏TEST、GTEST_TEST、TEST_F、TEST_P、GTEST_TEST_等实现的测试用例注册到测试集中便于后期遍历测试;

TEST:跟踪宏TEST(test_case_name,test_name)->GTEST_TEST(test_case_name, test_name)->GTEST_TEST_(test_case_name, test_name,::testing::Test,::testing::internal::GetTestTypeId())->class GTEST_TEST_CLASS_NAME_(test_case_name,test_name):public parent_class;

此外宏GTEST_TEST_CLASS_NAME_(test_case_name,test_name)-> test_case_name##_##test_name##_Test;可以看出宏TEST被封装为一个类,父类为::testing::Test,类名称test_case_name_test_name_Test,类提供静态成员变量::testing::TestInfo*类型的test_info_,另外还有TestBody虚函数,这个函数作为整个测试体内容由用户测试实例实现;接着test_info_的初始化则通过内部函数MakeAndRegisterTestInfo创建了一个TestInfo对象并注册到test_info_list_,该函数参数中测试用例工厂函数TestFactoryImpl创建了本类测试用例对象的一个实例且用本类的相关参数初始化创建的test_info_对象,紧接着该test_info_对象通过AddTestInfo被添加到UnitTest单例下的UnitTestImpl对象下且根据test_case_name的不同添加到类型为vector<TestCase *>的test_cases_容器中,创建的TestCase对象内部的通过函数AddTestInfo才是真正的添加test_info_对象的操作,并保存于test_info_list_容器中,具体的布局为:

UnitTest                      (单例对象)

  |-UnitTestImpl              (对象成员(事实上也为单例))

      |-test_cases_           (vector<TestCase*>保存左参数不同的容器)

          |-test_info_list_   (vector<TestInfo*>保存右参数不同的容器)

该二级容器中事实上测试用例遍历的是每个test_cases_下的test_info_list_,左参数容器主要作test_case_name不同的区分,右参数容器保存相同左参数test_case_name容器对象下的不同右参数test_name测试用例;此外::testing::Test类中的比较常用的函数如:static void SetUpTestCase(),static void TearDownTestCase(),virtual void SetUp(),virtual void TearDown(),一般这几个函数都会用到,不过在当前宏下无法用到,可在宏TEST_F和TEST_P下可能用到;

TEST_F:跟踪宏TEST_F(test_fixture,test_name)->GTEST_TEST_(test_fixture, test_name, test_fixture,::testing::internal::GetTypeId<test_fixture>())此后部分同TEST宏的后部分,与TEST宏的区别在于TEST_F下的新的类是以左参数test_fixture作为父类,并一定是::testing::Test(也可以是);

TEST_P:跟踪宏TEST_P(test_case_name,test_name)->class GTEST_TEST_CLASS_NAME_(test_case_name,test_name):public test_case_name 此宏类名仍然为test_case_name_test_name_Test,父类为test_case_name,类中成员有静态成员AddToRegistry、TestBody,静态成员变量gtest_registering_dummy_,该静态变量初始化通过AddToRegistry函数实现,接下来在AddToRegistry函数内部调用了UnitTest单例下UnitTestImpl的parameterized_test_registry成员函数获取到其parameterized_test_registry_参数化测试注册对象;并通过该注册对象的GetTestCasePatternHolder函数创建typed_test_info对象并添加至类型为vector<ParameterizedTestCaseInfoBase*>的test_case_infos_容器中,typed_test_info对象通过调用AddTestPattern创建TestInfo对象并添加至类型为vector<linked_ptr<TestInfo>>的容器中,此外AddTestPattern参数中通过::testing::internal::TestMetaFactory创建了本类测试用例对象;可以看到此与宏TEST/TEST_F的不一样的创建工厂,该类的基类为TestMetaFactoryBase;不过共同点是最后的测试用例均为同一的TestInfo类型,这样便于遍历的时候统一处理、无区别对待;

具体的布局为:

UnitTest                      (单例对象)

  |-UnitTestImpl              (对象成员(也为单例))

      |-ParameterizedTestCaseRegistry (参数化测试注册对象(单例))

          |-test_case_infos_(vector<ParameterizedTestCaseInfoBase*>保存左参数不同的容器)

              |-tests_ (vector<linked_ptr<TestInfo> >保存右参数不同的容器)

INSTANTIATE_TEST_CASE_P:带参数测试实例宏,跟踪宏INSTANTIATE_TEST_CASE_P(prefix, test_case_name, generator)内部定义了一个gtest_##prefix##test_case_name##_EvalGenerator_()的全局函数,返回值为::testing::internal::ParamGenerator<test_case_name::ParamType>类型的参数生成器,此外还有一个int类型的gtest_##prefix##test_case_name##_dummy_全局变量,该变量的初始化同样获取UnitTest单例下UnitTestImpl的parameterized_test_registry成员函数获取到其parameterized_test_registry_参数化测试注册对象;并通过该注册对象的AddTestCaseInstantiation函数实现对test_case_infos_容器对象中的instantiations_容器添加std::pair<string, GeneratorCreationFunc*>对象;

具体的布局为:

UnitTest                      (单例对象)

  |-UnitTestImpl              (对象成员(也为单例))

      |-ParameterizedTestCaseRegistry (参数化测试注册对象(单例))

          |-test_case_infos_(vector<ParameterizedTestCaseInfoBase*>保存test_case_name参数不同的容器)

               |-instantiations_ (vector<std::pair<string, GeneratorCreationFunc*> >保存参数prefix与generator对的容器)

注册测试中遍历测试用例:在遍历测试用例前,会执行InitGoogleTest,该函数内部调用RegisterParameterizedTests注册带参数化的测试用例,前面已看到宏TEST与INSTANTIATE_TEST_CASE_P保存的测试用例不在同一个地方,为了便于后续遍历用例,此处统一注册所有用例至test_cases_容器以及其下的容器test_info_list_中,RegisterParameterizedTests内部调用RegisterTests,并且将test_case_infos_容器中每个对象均调用RegisterTests实现tests_包含instantiations_两个容器中的测试用例注册到test_cases_容器以及其下的容器test_info_list_中(instantiations_转化为

TestInfo对象的参数使用创建工厂CreateTestFactory),若tests_下的instantiations_容器不存在测试用例则不转化,处理后目前有两套测试容器

宏TEST(TEST_F、INSTANTIATE_TEST_CASE_P已合并容器)、宏TEST_P;但是TEST_P宏是为INSTANTIATE_TEST_CASE_P宏服务的,故再遍历执行测试用例的时候可以不再需要它下面的容器用例了,此外TEST_P中GetParam()函数可获取到当前实例的参数,该参数由INSTANTIATE_TEST_CASE_P宏中的generator参数生成器获得该对应参数值;

遍历测试用例容器:RUN_ALL_TESTS()-->::testing::UnitTest::GetInstance()->Run()-->HandleExceptionsInMethodIfSupported()-->internal::UnitTestImpl::RunAllTests()-->

顺序遍历SetUpEnvironment()-->顺序遍历test_cases_->Run()-->SetUpTestCase()->顺序遍历test_info_list_->Run()-->SetUp()-->TestBody()-->TearDown()-->

TearDownTestCase()-->倒序遍历TearDownEnvironment();

以上为所有大体的执行顺序,在测试中可以发现该流程和使用教程中提到的顺序一致的,其中部分调用细节后面会分析到;

断言宏:

  提供了丰富的断言宏如ASSERT_EQ、ASSERT_LE、EXPECT_STREQ、EXPECT_FLOAT_EQ、

EXPECT_PRED_FORMAT3等(当为两个参数时,一般左参数为期望值、右参数为实际值或者为左参数为一个自定义比较函数,右参数为比较值;对于一个参数的即为条件值);基本上分为以ASSERT_XX和EXPECT_XX两大类,其中以ASSERT_XX表示若失败不再执行后续测试用例跳出TestBody函数而EXPECT_XX则可继续执行;

以下以跟踪宏ASSERT_EQ为例,ASSERT_EQ(val1, val2)--> GTEST_ASSERT_EQ(val1, val2)-->ASSERT_PRED_FORMAT2(::testing::internal::EqHelper<GTEST_IS_NULL_LITERAL_(expected)>::Compare,expected,actual)-->GTEST_PRED_FORMAT2_(pred_format,v1,v2, GTEST_FATAL_FAILURE_)-->GTEST_ASSERT_(pred_format(#v1, #v2, v1, v2),on_failure)

内部比较使用了Compare函数,其他的宏ASSERT_NE、ASSERT_GT、ASSERT_LT等则分别对应CmpHelperNE、CmpHelperGT、CmpHelperLT等,EXPECT_XX系列宏则也类似,只是在扩展宏时使用的是GTEST_PRED_FORMAT2_(pred_format, v1, v2, GTEST_NONFATAL_FAILURE_);

可以发现以上扩展宏的时候,部分宏可以供用户提供自定义的比较函数或操作函数而不是使用内置的比较函数,如上述宏ASSERT_PRED_FORMAT2,替换第一个参数为用户自定义函数,该函数应满足形如EqHelper的形式即可,需要其他的宏ASSERT_PRED_FORMAT1、EXPECT_PRED_FORMAT1、EXPECT_PRED_FORMAT2、ASSERT_PRED1、ASSERT_PRED2、EXPECT_PRED1、EXCPET_PRED2;此外还有一些其他比较有用的宏如GTEST_FAIL、GTEST_SUCCEED、SUCCEED、ADD_FAILURE等也是使用了上述扩展时用到的宏的重声明宏的变体而已,另外还有异常检查宏、BOOLEAN宏、字符串比较宏、浮点数比较宏、Windows HRESULT检查宏、编译时类型检查StaticAssertTypeEq(内部使用了StaticAssertTypeEqHelper<T,T>辅助模板工具);当然还有其他有用的宏,不再一一列举;额外功能,各种断言宏因内部保存有::testing::AssertionResult的执行结果,该对象内部维护了结果信息message_并重载了operator<<操作符(内部调用了message_的append方法),使得可以直接追加自定义信息,使用方式如:EXPECT_EQ(2, Foo(4, 10))<<”our custom info” << 13;

全局环境:

  在遍历测试用例容器中,进入正式测试用例前首先顺序循环遍历了环境容器执行

SetUpEnvironment()(内部调用了SetUp()安装环境),测试完所有用例后执行了倒序遍历环境容器TearDownEnvironment()(内部调用了TearDown()卸载环境);要实现全局环境,必须写一个类,继承testing::Environment类,实现里面的SetUp和TearDown方法;且在调用InitGoogleTest前通过testing::AddGlobalTestEnvironment()添加到全局测试的环境中;

死亡测试:

  死亡测试在一个安全的环境下执行崩溃的测试案例,同时又对崩溃结果进行验证,宏ASSERT_DEATH、EXPECT_DEATH、ASSERT_EXIT、EXPECT_EXIT等,事实上测试崩溃内部调用abort();此外死亡测试宏中左参数已以*DeathTest命名,因内部对该参数作了判断,若存在此“DeathTest”结尾的测试用例,被插入到test_cases_容器的前端,更多的死亡测试依次插入到非死亡测试前端,使得测试用例执行时先执行死亡测试,死亡测试对象通过death_test_factory()工厂函数创建;此外应根据需要在main中或TestBody中重定义标识GTEST_FLAG(death_test_style)为"threadsafe"或"fast";

运行参数、xml文件保存、控制台颜色控制:

      通过之前的执行流程,我们知道运行参数可以通过环境变量、命令行参数、以及手动设置标识Flag设置,具体可以设置哪些选项以及设置哪些类型值可直接参考gtest.h文件首部定义的一系列标识如:GTEST_DECLARE_bool_(also_run_disabled_tests)等;设置XML文件保存可通过设置标识testing::GTEST_FLAG(output) = "xml:";值可以为文件目录或文件路径名(Examples: "xml:filename.xml","xml::directoryname/")若不指定名称,则默认为可执行文件名称;控制台颜色控制采用GetStdHandle、

GetConsoleScreenBufferInfo、SetConsoleTextAttribute函数实现不同色彩的输出控制,

可自定义色彩输出控制,设置标识GTEST_FLAG(color)(可选值yes,no,auto);

总结:

  GTest项目采用了大量的宏、模板实现,此外也利用了C++语言和VC编译器特性实现各测试用例的创建和注册功能,实现了无需手动注册;此外因使用模板以及初始化构建,则编译会花费一定的时间;另外全局变量、静态变量和各测试用例(均是new出来的),对于项目内部需要处理好资源释放的问题,提供了多种测试用例方式(一般测试、参数化测试、死亡测试)、对异常、断言支持丰富、多种控制选项标识、多种实用的宏以及支持XML文件保存(只保存测试失败时的信息);整体上封装得已很简单,用户可以直接把重心转移到写测试用例即可,工作量会比较少;此处更多的详细内部细节未作分析(因宏与模板比较难以跟踪,此处只了解整个大体流程、内容即可);

原文地址:https://www.cnblogs.com/haomiao/p/5041055.html