Google Test参数化测试:能否避免重复定义测试夹具?
我有一个被测类MyClass,输入为字符串,内部会将其转换为整数,需要测试三类场景:
- 合法范围内的数值
- 合法范围外的数值
- 无效类型的数值
当前采用的Google Test实现结构如下:
// Class under test class MyClass { // Class contents public: void MyFunction(std::string input) { // Do stuff. } }; class MyClassBaseTest : public ::testing::TestWithParam<std::string> {}; // In bound values class MyClassOKTest : public MyClassBaseTest {}; INSTANTIATE_TEST_SUITE_P(OKValues, MyClassOKTest, testing::Values( "1", "2", "3" )); TEST_P(MyClassOKTest, OKValuesTest) { MyClass sut; } // Out of bound values class MyClassOutOfBoundTest : public MyClassBaseTest {}; INSTANTIATE_TEST_SUITE_P(OutOfBound, MyClassOutOfBoundTest, testing::Values( "30", "40", "50" )); TEST_P(MyClassOutOfBoundTest, OutOfBoundValuesTest) { MyClass sut; } // Invalid values values class MyClassInvalidTest : public MyClassBaseTest {}; INSTANTIATE_TEST_SUITE_P(Invalid, MyClassInvalidTest, testing::Values( "3.0", "Foo", "Bar" )); TEST_P(MyClassInvalidTest, InvalidValuesTest) { MyClass sut; }
我希望实现更简洁的结构,避免重复定义测试夹具,比如类似下面的写法(但看起来Google Test不支持):
// Class under test class MyClass { // Class contents public: void MyFunction(std::string input) { // Do stuff. } }; // In bound values class MyClassTest : public ::testing::TestWithParam<ParamType> {}; INSTANTIATE_TEST_SUITE_P(OKValues, MyClassOKTest, testing::Values( "1", "2", "3" )); TEST_P(OKValues, OKValuesTest) { MyClass sut; } // Out of bound values INSTANTIATE_TEST_SUITE_P(OutOfBound, MyClassOutOfBoundTest, testing::Values( "30", "40", "50" )); TEST_P(OutOfBound, OutOfBoundValuesTest) { MyClass sut; } // Invalid values values INSTANTIATE_TEST_SUITE_P(Invalid, MyClassInvalidTest, testing::Values( "3.0", "Foo", "Bar" )); TEST_P(Invalid, InvalidValuesTest) { MyClass sut; }
我的疑问:
- 初始方案是不是唯一可行的方式?有没有遗漏Google Test的特性?
- 有没有更简洁的实现结构可以推荐?
首先,你期望的直接用INSTANTIATE_TEST_SUITE_P的名称作为TEST_P第一个参数的写法,Google Test确实不支持。TEST_P的第一个参数必须是继承自TestWithParam的测试夹具类,而非实例化套件的名称,这是框架的设计规则。
初始方案是可行的,但并非唯一方式,以下是几种更简洁的优化方向:
1. 用参数结构体合并所有测试场景
定义一个包含输入字符串和测试类型标记的结构体作为参数,只需要一个测试夹具和一个TEST_P函数,在测试逻辑中根据标记区分不同场景的断言:
#include <string> #include <gtest/gtest.h> class MyClass { public: void MyFunction(std::string input) { // 业务实现逻辑 } }; // 标记测试场景类型 enum class TestType { OK, OutOfBound, Invalid }; // 参数结构体:封装输入值和场景类型 struct TestParam { std::string input; TestType type; }; // 唯一的测试夹具 class MyClassTest : public ::testing::TestWithParam<TestParam> {}; // 一次性实例化所有测试参数 INSTANTIATE_TEST_SUITE_P(AllTestScenarios, MyClassTest, testing::Values( TestParam{"1", TestType::OK}, TestParam{"2", TestType::OK}, TestParam{"3", TestType::OK}, TestParam{"30", TestType::OutOfBound}, TestParam{"40", TestType::OutOfBound}, TestParam{"50", TestType::OutOfBound}, TestParam{"3.0", TestType::Invalid}, TestParam{"Foo", TestType::Invalid}, TestParam{"Bar", TestType::Invalid} )); TEST_P(MyClassTest, VerifyAllScenarios) { MyClass sut; const auto& param = GetParam(); switch(param.type) { case TestType::OK: // 合法值的断言逻辑 ASSERT_NO_THROW(sut.MyFunction(param.input)); // 其他针对合法值的验证 break; case TestType::OutOfBound: // 越界值的断言逻辑 ASSERT_THROW(sut.MyFunction(param.input), std::out_of_range); break; case TestType::Invalid: // 无效值的断言逻辑 ASSERT_THROW(sut.MyFunction(param.input), std::invalid_argument); break; } }
这种方式的优势是只需要维护一个夹具和测试函数,所有参数集中管理;缺点是测试逻辑会集中在一个函数内,场景较多时可能略显冗长,但可读性仍有保障。
2. 用typedef简化子类定义
如果希望保留不同测试函数的独立性,可通过typedef简化初始方案中的子类定义,避免重复的类继承代码:
#include <string> #include <gtest/gtest.h> class MyClass { public: void MyFunction(std::string input) { // 业务实现逻辑 } }; // 基础测试夹具 class MyClassBaseTest : public ::testing::TestWithParam<std::string> {}; // 用typedef简化子类定义,替代重复的类继承声明 typedef MyClassBaseTest MyClassOKTest; typedef MyClassBaseTest MyClassOutOfBoundTest; typedef MyClassBaseTest MyClassInvalidTest; // 合法值测试实例化与测试函数 INSTANTIATE_TEST_SUITE_P(OKValues, MyClassOKTest, testing::Values("1", "2", "3")); TEST_P(MyClassOKTest, ValidateOKValues) { MyClass sut; // 合法值测试逻辑 ASSERT_NO_THROW(sut.MyFunction(GetParam())); } // 越界值测试实例化与测试函数 INSTANTIATE_TEST_SUITE_P(OutOfBoundValues, MyClassOutOfBoundTest, testing::Values("30", "40", "50")); TEST_P(MyClassOutOfBoundTest, ValidateOutOfBoundValues) { MyClass sut; // 越界值测试逻辑 ASSERT_THROW(sut.MyFunction(GetParam()), std::out_of_range); } // 无效值测试实例化与测试函数 INSTANTIATE_TEST_SUITE_P(InvalidValues, MyClassInvalidTest, testing::Values("3.0", "Foo", "Bar")); TEST_P(MyClassInvalidTest, ValidateInvalidValues) { MyClass sut; // 无效值测试逻辑 ASSERT_THROW(sut.MyFunction(GetParam()), std::invalid_argument); }
这种方式保留了不同测试场景的独立性,同时消除了冗余的类继承代码,是初始方案的轻量化版本,可读性和维护性都不错。
3. 其他进阶方式(针对复杂场景)
如果需要更灵活的参数组合,可使用testing::Combine结合不同参数源,但对于你的场景,前面两种方式已经足够简洁实用。
总结:你期望的无重复夹具的写法确实不被Google Test支持,但可以通过参数结构体合并场景或typedef简化子类的方式优化代码结构,既保证可读性又减少冗余。
内容的提问来源于stack exchange,提问作者Jan Jaap

