单元测试框架GTest的自动调用机制分析
2019-05-25刘艳平费琪陈伟
刘艳平, 费琪, 陈伟
(1. 91404部队, 秦皇岛 066000; 2. 江苏自动化研究所, 连云港 222061)
0 引言
单元测试是用来对独立单元的某些假设验证的测试工作。一个独立的单元可以是一个模块、一个函数或者一个类[1]。单元测试可以在源头发现和修正软件缺陷,充分的单元测试对软件系统的集成和稳定运行具有重要意义。当前的软件的单元测试主要通过单元测试框架完成,主流的单元测试框架有Junit[2]、CppUnit、C++Test、XUnit、unittest、PHPUnit和GoogleTest[3]。
当前的对单元测试的研究主要内容包括测试数据的生成方法和框架的使用方法,如文献[4]研究了JUnit在单元测试中的应用,文献[5]研究了退火算法生成单元测试输入数据。当前关于单元测试框架的原理和实现机制的研究较少,对框架原理的深入研究有利于测试工具的再开发[6]。
1 单元测试框架Gtest
GoogleTest简称为GTest,是一个跨平台的、自动化、开源的C++单元测试框架。GTest具有以下鲜明特点:
1.灵活的宏机制和丰富的断言集。宏机制提高了GTest的抽象性和通用性,使得测试代码具有很强的一致性,丰富的断言集可支持不同的测试任务,可支持整型、浮点数、字符数等多种类型数值的比较;
2.具有灵活的事件机制,通过事件机制可以监测测试的进度和状态,可自定义测试的全局、TestSuite、TestCase等级别的环境参数,完成测试数据和状态的初始化和自定义;
3.具有参数化功能,参数组合和自动入参机制支持一次设置同一类型的参数和不同类型的参数,很好地支持大批量测试数据的输入[7]。
2 Gtest的自动调用机制
2.1 预编译过程
GTest的测试代码通常是通过宏的方式实现的。因此预编译过程需要实现宏替换,经过宏替换生成以测试用例名和测试特例名命名的唯一类GTEST_TEST_CLASS_NAME_Test,类的静态成员test_info_在main函数运行之前运行,完成测试信息的注册,测试用例的注册信息保存到列表UnitTestImpl:: test_cases_中,测试特例的信息保存到列表TestCase:: test_info_list_[8]。以GTest自带的Sample1为例,TEST宏替换过程如下图1所示。
2.2 测试执行过程
单元测试从main() 函数开始运行,其中的RUN_ALL_TESTS()启动所有测试,RUN_ALL_TESTS()的实现如下:
inline int RUN_ALL_TESTS() {
return ::testing::UnitTest::GetInstance()->Run();
}
图1 GTest预编译宏替换过程
UnitTest类为单例模式,在一个测试项目中只有一个UnitTest类的实例,是连接所有逻辑的中枢。UnitTest的Run方法和核心逻辑代码如下:
return internal::HandleExceptionsInMethodIf Supported(
impl(),
&internal::UnitTestImpl::RunAllTests,
"auxiliary test code (environments or event listeners)") ? 0 : 1;
}
impl()返回一个UnitTestImpl的对象指针impl_,UnitTestImpl是单例模式,对象指针impl_即是本次测试的实例。函数HandleExceptionsInMethodIfSupported的核心部分如下:
template
Result HandleExceptionsInMethodIfSupported(
T* object, Result (T::*method)(), const char* location) {
return (object->*method)(); }
即HandleExceptionsInMethodIfSupported(impl(),&internal::UnitTestImpl::RunAllTests, …) 相当于impl_->RunAllTests();运行机制如图2所示。
UnitTestImpl:: GetMutableTestCase(test_index)->Run(),逐个返回UnitTestImpl对象成员变量test_cases_中的元素,然后调用TestCase的Run方法,定义如下所示:
TestCase* GetMutableTestCase(int i) {
const int index = GetElementOr(test_case_indices_, i, -1);
return index < 0 ? NULL : test_cases_[index];
}
图2 RUN_ALL_TEST的运行逻辑
TestCase的Run方法实现机制如图3所示。
图3 TestCase::Run()的运行逻辑
代码的核心功能包括监听当前TestCase的信息和调用TestInfo的Run()方法。
类TestInfo的Run 方法首先创建类test_case_name_test_name_Test(用例名_实例名_TEST)的实例(测试特例),之后调用test_case_name_test_name_Test的父类Test的Run方法,Testing::Run()先完成特例级别的数据预处理,然后调用Testing::TestBody(),而Testing::TestBody()就是用户自定义的具体的测试内容,具体逻辑如图4所示。
2.3 Linsten监听测试过程
上文提到Gtest监听UnitTest、TestCase、Test的运行的
图4 TestInfo::Run()的运行逻辑
开始和结束,输出对应测试测试、测试用例、测试特例的信息到控制台。Listen机制的具体实现由类PrettyUnitTestResultPrint、TestEventListen、TestEventListens、TestEventRepeater实现,类UnitTestIMPl、TestInfo、TestCase是类TestEventListens的友元类,获取当前的监听对象(Listeners),类之间的关系如图5所示。
图5 GTest的Listen机制类层次关系
TestEventListener是基类,定义了事件响应函数OnTestProgramStart等纯虚函数,PrettyUnitTestResultPrinte继承TestEventListener,实现了具体的逻辑;TestEventListeners 包含成员变量TestEventRepeater* repeater_,TestEventRepeater继承自TestEventListener,同时包含成员变量类TestEventListener对象指针列表listeners_,listeners_保存当前的监听者; TestEventListeners的友元类UnitTestIMpl、TestCase、TestInfo能够直接访问TestEventListener的私有成员repeater_,启动监听任务:
UnitTestImpl::UnitTestImpl(UnitTest* parent)
{
listeners()->SetDefaultResultPrinter(new PrettyUnitTestResultPrinter);
}
UnitTestImpl的成员函数listeners()返回当前的监听者列表,SetDefaultResultPrinter函数内部调用Append(listener), Append()通过repeater_->Append(listener)更新listener到TestEventRepeater的监听者listeners_中,完成初始化工作;
在RUN_ALL_TEST()、TestCase::Run()、TestInfo::Run()等函数内部通过单例模式机制获取当前测试中的中继者repeater,TestEventRepeater是整个监听机制的中继者,通过Append(Listen)和Release(Listen)改变listeners_,设置当前的监听对象列表,TestEventRepeater实现基类的OnTestProgramStart等纯虚方法,将监听到的消息转发listeners_中的具体listenter的对应方法处理,完成具体业务。具体过程是通过下述宏实现的。
#define GTEST_REPEATER_METHOD_(Name, Type)
void TestEventRepeater::Name(const Type& parameter) {
for (size_t i = 0; i < listeners_.size(); i++)
listeners_[i]->Name(parameter); }
例如GTEST_REPEATER_METHOD_(OnTestProgramStart, UnitTest)即TestEventRepeater::OnTestProgramStart(UnitTest & parameter),repeater_->OnTestCaseStart等函数的调用即是调用listeners_中各Listener的OnTestCaseStart等函数,也就是PrettyUnitTestResultPrinter的OnTestCaseStart等函数,在这些函数中获取当前测试的信息和状态并输出到结果文件中,完成监听任务。
3 总结
软件测试中,单元测试是发现软件缺陷最早的时刻,也是修改缺陷成本最低的时候,深入理解并掌握测试框架对于提高软件质量和开发速度具有重要意义。GTest是一个结构成熟、功能完善、使用广泛的测试框架,体现了世界上先进的程序员的开发技术,本文研究的自动调用机制是其最基础的机制,GMock技术、死亡测试技术、参数化测试等高级测试技术仍值得继续深入研究[9]。