18、Google Test高级特性:测试夹具、参数化测试、类型参数化测试
说实话,很多C++开发者写单元测试,还停留在「写个函数、assert一下」的阶段。但真实项目哪有这么简单?你想想看,一个类可能有几十种初始状态,一个接口要兼容五六种数据类型。如果每个case都手写一遍测试函数,那代码量直接爆炸。
今天我就带你看看Google Test的三个高级武器——测试夹具、参数化测试、类型参数化测试。这些工具我用过不下上百次,每次都能让测试代码量减少一半以上。
18.1 测试夹具(Test Fixtures)——别再重复造轮子了
先说说测试夹具。说白了,它就是帮你把「准备环境」和「清理环境」这两件事封装起来。我见过太多人每个测试函数里都写一遍new和delete,看着就累。
Google Test里,测试夹具就是一个继承自::testing::Test的类。你只需要重写SetUp()和TearDown()两个虚函数就行。
class MyDatabaseTest : public ::testing::Test {
protected:
Database* db;
void SetUp() override {
db = new Database("test.db");
db->connect();
db->initTables();
}
void TearDown() override {
db->close();
delete db;
}
};
然后测试用例用TEST_F宏,而不是TEST宏。注意,TEST_F的第一个参数是夹具类名,第二个才是测试名。
TEST_F(MyDatabaseTest, InsertWorks) {
db->insert("user", "alice");
auto result = db->query("SELECT * FROM user");
EXPECT_EQ(result.size(), 1);
}
TEST_F(MyDatabaseTest, DeleteWorks) {
db->insert("user", "bob");
db->deleteRecord("user", "bob");
auto result = db->query("SELECT * FROM user");
EXPECT_EQ(result.size(), 0);
}
每个TEST_F执行前都会调用SetUp(),执行后调用TearDown()。所以两个测试互不干扰。我在项目中遇到过一个问题:有人把数据库连接写在全局变量里,结果测试A改了数据,测试B就挂了。用了夹具之后,这种问题再也没出现过。
SetUp()和TearDown()的开销很大(比如启动一个服务器),你可以考虑用SetUpTestSuite()和TearDownTestSuite(),它们在整个测试套件执行前后只调用一次。
18.2 参数化测试——一个测试跑遍所有输入
接下来是参数化测试。这个功能太实用了。你想想看,一个排序函数,你要测空数组、单元素、逆序、重复元素……如果每个case写一个TEST,那得写到什么时候?
Google Test提供了TEST_P宏和INSTANTIATE_TEST_SUITE_P宏来解决这个问题。
首先,你需要一个继承自::testing::TestWithParam<T>的夹具类,其中T是参数类型。
class SortTest : public ::testing::TestWithParam<std::vector<int>> {
protected:
void SetUp() override {
input = GetParam();
}
std::vector<int> input;
};
然后写测试用例:
TEST_P(SortTest, SortsCorrectly) {
auto result = mySort(input);
EXPECT_TRUE(std::is_sorted(result.begin(), result.end()));
EXPECT_EQ(result.size(), input.size());
}
最后,用INSTANTIATE_TEST_SUITE_P来提供参数列表:
INSTANTIATE_TEST_SUITE_P(
SortVariants,
SortTest,
::testing::Values(
std::vector<int>{},
std::vector<int>{1},
std::vector<int>{3, 2, 1},
std::vector<int>{1, 2, 3, 4, 5},
std::vector<int>{5, 4, 3, 2, 1}
)
);
这样,一个TEST_P就会自动生成5个测试用例。每个用例的名字会自动带上参数索引,比如SortVariants/SortTest.SortsCorrectly/0。如果某个参数挂了,你能立刻定位到是哪个输入出了问题。
18.3 类型参数化测试——一个测试兼容所有类型
类型参数化测试,说白了就是参数化测试的「类型版本」。它不是为了测不同输入,而是为了测不同数据类型。
举个例子,你写了一个通用的Stack<T>类。你要确保它对int、double、string都能正常工作。难道每个类型写一套测试?
Google Test提供了TYPED_TEST_SUITE和TYPED_TEST来解决这个问题。
首先,定义一个模板化的夹具:
template <typename T>
class StackTest : public ::testing::Test {
protected:
Stack<T> stack;
};
然后声明要测试的类型列表:
using MyTypes = ::testing::Types<int, double, std::string>;
TYPED_TEST_SUITE(StackTest, MyTypes);
最后写测试用例。注意,这里用TYPED_TEST宏,并且在测试内部用TypeParam来引用当前类型。
TYPED_TEST(StackTest, PushPopWorks) {
TypeParam value = TypeParam(); // 默认构造
this->stack.push(value);
EXPECT_EQ(this->stack.pop(), value);
EXPECT_TRUE(this->stack.isEmpty());
}
TYPED_TEST(StackTest, TopReturnsLastElement) {
TypeParam a = TypeParam(42); // 假设支持int构造
TypeParam b = TypeParam(100);
this->stack.push(a);
this->stack.push(b);
EXPECT_EQ(this->stack.top(), b);
}
这里有个坑,我踩过好几次:在TYPED_TEST里访问夹具的成员时,必须用this->前缀。因为TYPED_TEST生成的类是一个模板派生类,不写this->会导致依赖名称查找失败。嗯,这个语法细节很容易被忽略。
SetUpTestSuite和TearDownTestSuite。如果你需要全局初始化,可以考虑用SetUp()配合静态标志位来实现。
18.4 三者对比与选型建议
这三种特性各有各的适用场景。我整理了一个表格,方便你快速决策:
| 特性 | 适用场景 | 宏/关键字 | 典型例子 |
|---|---|---|---|
| 测试夹具 | 多个测试共享相同的环境准备和清理逻辑 | TEST_F |
数据库测试、网络连接测试 |
| 参数化测试 | 同一个逻辑,不同输入值 | TEST_P, INSTANTIATE_TEST_SUITE_P |
排序算法、字符串解析 |
| 类型参数化测试 | 同一个逻辑,不同数据类型 | TYPED_TEST, TYPED_TEST_SUITE |
容器类、泛型算法 |
我个人习惯是:先看有没有「环境准备」的重复代码,有就用夹具。再看有没有「输入变化」的需求,有就用参数化。最后看是不是「类型变化」,是就用类型参数化。这三个工具组合起来,基本能覆盖90%的测试场景。
18.5 知识体系总览
下面这张图帮你理清这三个特性的关系和适用边界:
18.6 实战中的避坑指南
最后,分享几个我踩过的坑:
- 夹具成员必须是protected或public:
TEST_F生成的类继承自夹具,private成员访问不到。我刚开始写的时候就把成员设成private,编译报错半天没反应过来。 - 参数化测试的参数类型必须支持拷贝:因为Google Test内部会复制参数。如果你传了一个unique_ptr,编译直接报错。可以用指针或者shared_ptr绕过去。
- 类型参数化测试里别用静态局部变量:不同类型实例化出来的测试是独立的,但静态变量是共享的。我曾经在
TYPED_TEST里放了一个static计数器,结果不同类型之间互相干扰,查了好久才发现。 - INSTANTIATE_TEST_SUITE_P的名字不能重复:同一个测试套件里,如果你写了两个
INSTANTIATE_TEST_SUITE_P,第二个会覆盖第一个。我建议每个套件只写一个实例化块,把所有参数都放在里面。
好了,这三个高级特性就讲到这里。你可以在自己的项目里试试看,先从测试夹具入手,再慢慢引入参数化测试。相信我,用习惯了就再也回不去了。
公众号:蓝海资料掘金营,微信deep3321