写C++难免会遇到模板问题,如果要针对一个模板类进行测试,似乎之前博文中介绍的方式只能傻乎乎的一个一个特化类型后再进行测试。其实GTest提供了两种测试模板类的方法,本文我们将介绍方法的使用,并分析其实现原理。(转载请指明出于breaksoftware的csdn博客)
应用
GTest将这两种方法叫做:Typed Tests和Type-Parameterized Tests。我觉得可能叫做简单模式、高级模式比较通用。先不管这些名字吧,我们看看怎么使用
简单模式(Typed Tests)
首先我们要定义一个模板类。但是为了使用GTest框架,它要继承于Test类
template <typename T> class TypeTest : public Test { protected: bool CheckData() { if (typeid(T) == typeid(bool)) { return false; } else { return true; } } };
我们只实现了一个返回bool类型的模板类型判断函数CheckData。
然后我们使用下列方式定义一个类型,类型的模板参数是我们需要传递给TypeTest的模板类型
typedef testing::Types<int, long> IntegerTypes
接下来我们使用TYPED_TEST_CASE宏注册一个测试用例
TYPED_TEST_CASE(TypeTest, IntegerTypes);
最后我们使用TYPED_TEST_P定义一个测试特例
TYPED_TEST_P(TypeTest, Verify) { EXPECT_TRUE(CheckData()); };
如此我们可以对TypeTest<int>和TypeTest<long>进行测试。我们看下结果
[----------] 1 test from TypeTest/0, where TypeParam = int [ RUN ] TypeTest/0.Verify [ OK ] TypeTest/0.Verify (2454 ms) [----------] 1 test from TypeTest/0 (2455 ms total) [----------] 1 test from TypeTest/1, where TypeParam = long [ RUN ] TypeTest/1.Verify [ OK ] TypeTest/1.Verify (4843 ms) [----------] 1 test from TypeTest/1 (11093 ms total)
高级模式
如果我们还要对TypeTest使用其他类型作为模板参数进行测试,可能要这么写
typedef testing::Types<float, double> FloatTypes; TYPED_TEST_CASE(TypeTest, FloatTypes); // compile error
但是编译会报错,因为TYPED_TEST_CASE中定义的变量重定义了(之前TYPED_TEST_CASE(TypeTest, IntegerTypes);中定义的)。这个时候我们就要使用高级模式
首先我们需要声明一下测试用例类
TYPED_TEST_CASE_P(TypeTest);
然后使用TYPED_TEST_P定义一个测试实体
TYPED_TEST_P(TypeTest, Verify) { EXPECT_TRUE(CheckData()); };
接下来使用REGISTER_TYPED_TEST_CASE_P注册测试用例
REGISTER_TYPED_TEST_CASE_P(TypeTest, Verify);
最后使用INSTANTIATE_TYPED_TEST_CASE_P宏创建每个测试特例
INSTANTIATE_TYPED_TEST_CASE_P(IntegerCheck, TypeTest, IntegerTypes); INSTANTIATE_TYPED_TEST_CASE_P(FloatCheck, TypeTest, FloatTypes); INSTANTIATE_TYPED_TEST_CASE_P(BoolCheck, TypeTest, bool);
上面三行测试了三组类型,其输出是
[----------] 1 test from IntegerCheck/TypeTest/0, where TypeParam = int [ RUN ] IntegerCheck/TypeTest/0.Verify [ OK ] IntegerCheck/TypeTest/0.Verify (702 ms) [----------] 1 test from IntegerCheck/TypeTest/0 (703 ms total) [----------] 1 test from IntegerCheck/TypeTest/1, where TypeParam = long [ RUN ] IntegerCheck/TypeTest/1.Verify [ OK ] IntegerCheck/TypeTest/1.Verify (400 ms) [----------] 1 test from IntegerCheck/TypeTest/1 (403 ms total) [----------] 1 test from FloatCheck/TypeTest/0, where TypeParam = float [ RUN ] FloatCheck/TypeTest/0.Verify [ OK ] FloatCheck/TypeTest/0.Verify (451 ms) [----------] 1 test from FloatCheck/TypeTest/0 (453 ms total) [----------] 1 test from FloatCheck/TypeTest/1, where TypeParam = double [ RUN ] FloatCheck/TypeTest/1.Verify [ OK ] FloatCheck/TypeTest/1.Verify (403 ms) [----------] 1 test from FloatCheck/TypeTest/1 (405 ms total) [----------] 1 test from BoolCheck/TypeTest/0, where TypeParam = bool [ RUN ] BoolCheck/TypeTest/0.Verify ..\test\gtest_unittest.cc(117): error: Value of: CheckData() Actual: false Expected: true [ FAILED ] BoolCheck/TypeTest/0.Verify, where TypeParam = bool (5440 ms) [----------] 1 test from BoolCheck/TypeTest/0 (6633 ms total)
原理解析
简单模式
我们先从typedef testing::Types<int, long> IntegerTypes;这句开始分析。Types定义非常有意思,我截取部分来看看
struct Types0 {}; // Type lists of length 1, 2, 3, and so on. template <typename T1> struct Types1 { typedef T1 Head; typedef Types0 Tail; }; template <typename T1, typename T2> struct Types2 { typedef T1 Head; typedef Types1<T2> Tail; }; template <typename T1, typename T2, typename T3> struct Types3 { typedef T1 Head; typedef Types2<T2, T3> Tail; }; ...... template <typename T1 = internal::None, typename T2 = internal::None, typename T3 = internal::None, typename T4 = internal::None, typename T5 = internal::None, typename T6 = internal::None, typename T7 = internal::None, typename T8 = internal::None, typename T9 = internal::None, typename T10 = internal::None, typename T11 = internal::None, typename T12 = internal::None, typename T13 = internal::None, typename T14 = internal::None, typename T15 = internal::None, typename T16 = internal::None, typename T17 = internal::None, typename T18 = internal::None, typename T19 = internal::None, typename T20 = internal::None, typename T21 = internal::None, typename T22 = internal::None, typename T23 = internal::None, typename T24 = internal::None, typename T25 = internal::None, typename T26 = internal::None, typename T27 = internal::None, typename T28 = internal::None, typename T29 = internal::None, typename T30 = internal::None, typename T31 = internal::None, typename T32 = internal::None, typename T33 = internal::None, typename T34 = internal::None, typename T35 = internal::None, typename T36 = internal::None, typename T37 = internal::None, typename T38 = internal::None, typename T39 = internal::None, typename T40 = internal::None, typename T41 = internal::None, typename T42 = internal::None, typename T43 = internal::None, typename T44 = internal::None, typename T45 = internal::None, typename T46 = internal::None, typename T47 = internal::None, typename T48 = internal::None, typename T49 = internal::None, typename T50 = internal::None> struct Types { typedef internal::Types50<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31, T32, T33, T34, T35, T36, T37, T38, T39, T40, T41, T42, T43, T44, T45, T46, T47, T48, T49, T50> type; }; template <> struct Types<internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None> { typedef internal::Types0 type; }; template <typename T1> struct Types<T1, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None> { typedef internal::Types1<T1> type; }; template <typename T1, typename T2> struct Types<T1, T2, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None, internal::None> { typedef internal::Types2<T1, T2> type; };
这段代码很长,但是其定义了我们用于罗列类型的结构体Types。它是一个递归定义,即Types3依赖于Types2,Types2依赖于Types1,Types1依赖于Types0……。每个模板类都会将自己模板列表的第一个模板别名为Head,剩下的类型别名为Tail。未来我们将看到这两个类型的使用。
我们再看下TYPED_TEST_CASE的实现
# define TYPED_TEST_CASE(CaseName, Types) typedef ::testing::internal::TypeList< Types >::type GTEST_TYPE_PARAMS_(CaseName) # define GTEST_TYPE_PARAMS_(TestCaseName) gtest_type_params_##TestCaseName##_
它只是对该测试用例的参数列表类的type做了一个别名,展开代码就是
typedef ::testing::internal::TypeList< IntegerTypes >::type gtest_type_params_TypeTest_
TypeList是个模板类,它将Types<T>别名为type
template <typename T> struct TypeList { typedef Types1<T> type; };
对应于我们的例子就是
struct TypeList<IntegerTypes> { typedef Types1<IntegerTypes> type; };
于是整体展开就是
typedef ::testing::internal::Types1<IntegerTypes> gtest_type_params_TypeTest_
最后我们看下TYPED_TEST的宏实现。它和之前博文介绍的TEST宏有如下相同之处:
- 定义了私有的虚方法TestBody
- 执行了注册逻辑
- 末尾声明了TestBody函数部分,便于开发者填充测试实体
相同的地方我们就不说了,我们看下不同的
# define TYPED_TEST(CaseName, TestName) template <typename gtest_TypeParam_> class GTEST_TEST_CLASS_NAME_(CaseName, TestName) : public CaseName<gtest_TypeParam_> { private: typedef CaseName<gtest_TypeParam_> TestFixture; typedef gtest_TypeParam_ TypeParam; virtual void TestBody(); }; \
它继承于一个模板类,模板类的类名是我们通过TYPED_TEST传入的测试用例类。同时它将父类、模板类进行了别名操作。用我们的例子展开代码即是
template <typename T> class TypeTest_Verify_Test : public TypeTest<T> { private: typedef TypeTest<T> TestFixture; typedef T TypeParam; virtual void TestBody(); };
最后一个傀儡变量被初始化
bool gtest_##CaseName##_##TestName##_registered_ GTEST_ATTRIBUTE_UNUSED_ = ::testing::internal::TypeParameterizedTest< CaseName, ::testing::internal::TemplateSel< GTEST_TEST_CLASS_NAME_(CaseName, TestName)>, GTEST_TYPE_PARAMS_(CaseName)>::Register( "", ::testing::internal::CodeLocation(__FILE__, __LINE__), #CaseName, #TestName, 0); \
我们把代码展开
bool gtest_TypeTest_Verify_registered_ GTEST_ATTRIBUTE_UNUSED_ = ::testing::internal::TypeParameterizedTest<TypeTest, ::testing::internal::TemplateSel<TypeTest_Verify_Test>, gtest_type_params_TypeTest_ > ::Register( "", ::testing::internal::CodeLocation(__FILE__, __LINE__), 'TypeTest', 'Verify', 0);
我们看下TypeParameterizedTest类的Register实现
template <GTEST_TEMPLATE_ Fixture, class TestSel, typename Types> class TypeParameterizedTest { public: // 'index' is the index of the test in the type list 'Types' // specified in INSTANTIATE_TYPED_TEST_CASE_P(Prefix, TestCase, // Types). Valid values for 'index' are [0, N - 1] where N is the // length of Types. static bool Register(const char* prefix, CodeLocation code_location, const char* case_name, const char* test_names, int index) { typedef typename Types::Head Type; typedef Fixture<Type> FixtureClass; typedef typename GTEST_BIND_(TestSel, Type) TestClass; // First, registers the first type-parameterized test in the type // list. MakeAndRegisterTestInfo( (std::string(prefix) + (prefix[0] == '\0' ? "" : "/") + case_name + "/" + StreamableToString(index)).c_str(), StripTrailingSpaces(GetPrefixUntilComma(test_names)).c_str(), GetTypeName<Type>().c_str(), NULL, // No value parameter. code_location, GetTypeId<FixtureClass>(), TestClass::SetUpTestCase, TestClass::TearDownTestCase, new TestFactoryImpl<TestClass>); // Next, recurses (at compile time) with the tail of the type list. return TypeParameterizedTest<Fixture, TestSel, typename Types::Tail> ::Register(prefix, code_location, case_name, test_names, index + 1); } };
这段代码非常重要,我们看到核心函数MakeAndRegisterTestInfo,它将我们测试的对象加入到框架的执行队列中。具体它的原理和实现可以参看《Google Test(GTest)使用方法和源码解析——自动调度机制分析》。
第12行别名了Types::Head为Type。Types是传入的模板类,以我们的例子为例,其传入的就是::testing::internal::Types1<IntegerTypes>。我们在介绍Types模板类时提到过Head别名,它是该模板类第一个模板参数类型。对应于我们的例子就是typedef testing::Types<int, long> IntegerTypes;中的int类型。
第13行使用12行别名的类型,特化了我们传入的测试用例类,即该行对应于
typedef TypeTest<int> FixtureClass;
第14行对测试特例类使用了int类型进行特化
template <GTEST_TEMPLATE_ Tmpl> struct TemplateSel { template <typename T> struct Bind { typedef Tmpl<T> type; }; }; # define GTEST_BIND_(TmplSel, T) TmplSel::template Bind<T>::type
即对应于TypeTest_Verify_Test<T>::type
typedef typename TypeTest_Verify_Test<T> TestClass;
如此MakeAndRegisterTestInfo函数中的参数就比较明确了。
这段代码中还有个非常重要的一个递归调用
// Next, recurses (at compile time) with the tail of the type list. return TypeParameterizedTest<Fixture, TestSel, typename Types::Tail> ::Register(prefix, code_location, case_name, test_names, index + 1);
这次调用,最后一个模板参数传递了Types::Tail。它在编译期间将触发编译器进行类型推导,如同抽丝剥茧般,使用typedef testing::Types<int, long> IntegerTypes;中每个模板类型对TypeParameterizedTest::Register进行特化。从而可以在运行期间可以对每个类型进行注册。
高级模式
我们先看下TYPED_TEST_CASE_P宏的实现
# define TYPED_TEST_CASE_P(CaseName) static ::testing::internal::TypedTestCasePState GTEST_TYPED_TEST_CASE_P_STATE_(CaseName)
它定义了一个TypedTestCasePState类型的全局变量,对应于我们例子就是
static ::testing::internal::TypedTestCasePState gtest_typed_test_case_p_state_TypeTest_;
TypedTestCasePState类暴露了AddTestName方法用于保存测试用例和测试特例名 再看下REGISTER_TYPED_TEST_CASE_P宏的实现
# define REGISTER_TYPED_TEST_CASE_P(CaseName, ...) namespace GTEST_CASE_NAMESPACE_(CaseName) { typedef ::testing::internal::Templates<__VA_ARGS__>::type gtest_AllTests_; } static const char* const GTEST_REGISTERED_TEST_NAMES_(CaseName) = GTEST_TYPED_TEST_CASE_P_STATE_(CaseName).VerifyRegisteredTestNames( __FILE__, __LINE__, #__VA_ARGS__)
它使用TYPED_TEST_CASE_P定义的TypedTestCasePState类对象方法VerifyRegisteredTestNames获取已注册的测试用例名,并别名了一个类型。以上两个宏都是和测试用例名称注册有关。接下来我们看下TYPED_TEST_P的实现
# define TYPED_TEST_P(CaseName, TestName) namespace GTEST_CASE_NAMESPACE_(CaseName) { template <typename gtest_TypeParam_> class TestName : public CaseName<gtest_TypeParam_> { private: typedef CaseName<gtest_TypeParam_> TestFixture; typedef gtest_TypeParam_ TypeParam; virtual void TestBody(); }; static bool gtest_##TestName##_defined_ GTEST_ATTRIBUTE_UNUSED_ = GTEST_TYPED_TEST_CASE_P_STATE_(CaseName).AddTestName( __FILE__, __LINE__, #CaseName, #TestName); } template <typename gtest_TypeParam_> void GTEST_CASE_NAMESPACE_(CaseName)::TestName<gtest_TypeParam_>::TestBody()
它和我们之前介绍的TYPED_TEST实现是相似的。不同点是:
- 直接使用传入的测试特例名作为类名
- 调用TYPED_TEST_CASE_P定义的TypedTestCasePState类对象AddTestName对测试用例和测试特例名进行注册
- 将测试特例类和傀儡变量初始化过程控制在一个和测试用例名相关的命名空间中
最后我们看下INSTANTIATE_TYPED_TEST_CASE_P的实现
# define INSTANTIATE_TYPED_TEST_CASE_P(Prefix, CaseName, Types) bool gtest_##Prefix##_##CaseName GTEST_ATTRIBUTE_UNUSED_ = ::testing::internal::TypeParameterizedTestCase<CaseName, GTEST_CASE_NAMESPACE_(CaseName)::gtest_AllTests_, ::testing::internal::TypeList< Types >::type>::Register( #Prefix, ::testing::internal::CodeLocation(__FILE__, __LINE__), >EST_TYPED_TEST_CASE_P_STATE_(CaseName), #CaseName, GTEST_REGISTERED_TEST_NAMES_(CaseName))
可以说,套路和简单模式的注册方式是一样的。不一样的是它调用了TypeParameterizedTestCase类的Register,而不是TypeParameterizedTest的Register。还有就是Register的第二个参数是在REGISTER_TYPED_TEST_CASE_P别名的类型。我们看下这个类型的相关代码
# define REGISTER_TYPED_TEST_CASE_P(CaseName, ...) namespace GTEST_CASE_NAMESPACE_(CaseName) { typedef ::testing::internal::Templates<__VA_ARGS__>::type gtest_AllTests_; } \
// Template lists of length 1, 2, 3, and so on. template <GTEST_TEMPLATE_ T1> struct Templates1 { typedef TemplateSel<T1> Head; typedef Templates0 Tail; }; template <GTEST_TEMPLATE_ T1, GTEST_TEMPLATE_ T2> struct Templates2 { typedef TemplateSel<T1> Head; typedef Templates1<T2> Tail; };
template <> struct Templates<NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT> { typedef Templates0 type; }; template <GTEST_TEMPLATE_ T1> struct Templates<T1, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT> { typedef Templates1<T1> type; }; template <GTEST_TEMPLATE_ T1, GTEST_TEMPLATE_ T2> struct Templates<T1, T2, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT, NoneT> { typedef Templates2<T1, T2> type; };
可以见得这个类型和之前的Types是类似的,用于在编译期间通过编译器推导特例出注册方法。需要注意的是这个地方推导的不是模板类的类型,而是测试特例类。我们在讲解TYPED_TEST_P时提过,宏中直接使用传入的测试特例名作为类名,这是有原因的。原因就是在这儿要一个个推导。可能这么说还不太明白,我们看下高级模式的另外一种用法
TYPED_TEST_P(TypeTest, Verify) { EXPECT_TRUE(CheckData()); }; TYPED_TEST_P(TypeTest, Verify2) { EXPECT_FALSE(CheckData()); }; REGISTER_TYPED_TEST_CASE_P(TypeTest, Verify, Verify2);
这儿的Verify和Verify2都是测试特例名,于是通过REGISTER_TYPED_TEST_CASE_P操作后,就变成
typedef ::testing::internal::Templates<Verify,Verfiy2>::type gtest_AllTests_;
最后在下面注册函数中,触发对该函数使用Verify和Verfiy2进行特化的操作。
template <GTEST_TEMPLATE_ Fixture, typename Tests, typename Types> class TypeParameterizedTestCase { public: static bool Register(const char* prefix, CodeLocation code_location, const TypedTestCasePState* state, const char* case_name, const char* test_names) { std::string test_name = StripTrailingSpaces( GetPrefixUntilComma(test_names)); if (!state->TestExists(test_name)) { fprintf(stderr, "Failed to get code location for test %s.%s at %s.", case_name, test_name.c_str(), FormatFileLocation(code_location.file.c_str(), code_location.line).c_str()); fflush(stderr); posix::Abort(); } const CodeLocation& test_location = state->GetCodeLocation(test_name); typedef typename Tests::Head Head; // First, register the first test in 'Test' for each type in 'Types'. TypeParameterizedTest<Fixture, Head, Types>::Register( prefix, test_location, case_name, test_names, 0); // Next, recurses (at compile time) with the tail of the test list. return TypeParameterizedTestCase<Fixture, typename Tests::Tail, Types> ::Register(prefix, code_location, state, case_name, SkipComma(test_names)); } };
在TypeParameterizedTestCase类的Register方法中我们看到有对TypeParameterizedTest的Register的调用,于是两种方式打通了。一个测试特例下的类型推导是在TypeParameterizedTest的Register中完成的,而测试用例下不同测试特例的推导则在TypeParameterizedTestCase类的Register方法中完成的。