5、测试夹具(Test Fixtures):SetUp/TearDown的使用,共享测试数据,避免代码重复
写测试代码,最烦人的是什么?我个人觉得,是重复。
你想想看,每个测试用例都要创建同样的对象、初始化同样的数据、连接同样的资源。测试代码比业务代码还啰嗦,这谁受得了?
测试夹具(Test Fixtures)就是来解决这个问题的。说白了,它帮你把测试前的准备工作、测试后的清理工作,统一管理起来。一次写好,到处复用。
5.1 什么是测试夹具?
测试夹具,就是测试用例运行所需的「固定环境」。包括:
- 测试数据(比如一个预填充的容器)
- 对象实例(比如一个已配置的类对象)
- 资源状态(比如打开的文件、数据库连接)
在 Google Test 中,夹具通过继承 ::testing::Test 来实现。核心是两个虚函数:
SetUp():每个测试用例运行前调用,做准备工作TearDown():每个测试用例运行后调用,做清理工作
核心原则:SetUp 和 TearDown 是成对出现的。你在 SetUp 中 new 了什么,就在 TearDown 中 delete 什么。别只准备不收拾,内存泄漏就是这么来的。
5.2 一个简单的例子:从重复到复用
先看一个反面教材。假设我们要测试一个 UserManager 类:
// 没有夹具的写法 —— 重复代码满天飞
TEST(UserManagerTest, AddUser) {
UserManager manager;
manager.Init("test_db");
User u("Alice", 25);
manager.AddUser(u);
EXPECT_TRUE(manager.HasUser("Alice"));
}
TEST(UserManagerTest, RemoveUser) {
UserManager manager;
manager.Init("test_db");
User u("Bob", 30);
manager.AddUser(u);
manager.RemoveUser("Bob");
EXPECT_FALSE(manager.HasUser("Bob"));
}
看到了吗?每个测试用例都要创建 UserManager、调用 Init。如果 Init 的参数变了,你得改多少个地方?
用夹具改造一下:
class UserManagerTest : public ::testing::Test {
protected:
void SetUp() override {
manager = new UserManager();
manager->Init("test_db");
}
void TearDown() override {
manager->Close();
delete manager;
manager = nullptr;
}
UserManager* manager;
};
TEST_F(UserManagerTest, AddUser) {
User u("Alice", 25);
manager->AddUser(u);
EXPECT_TRUE(manager->HasUser("Alice"));
}
TEST_F(UserManagerTest, RemoveUser) {
User u("Bob", 30);
manager->AddUser(u);
manager->RemoveUser("Bob");
EXPECT_FALSE(manager->HasUser("Bob"));
}
注意看,这里用的是 TEST_F 而不是 TEST。F 就是 Fixture 的意思。每个 TEST_F 运行时,Google Test 都会:
- 创建一个新的夹具对象
- 调用
SetUp() - 执行你的测试用例
- 调用
TearDown() - 销毁夹具对象
每个测试用例之间,数据是完全隔离的。这一点很重要,我后面会细说。
5.3 共享测试数据:别踩这个坑
有些同学会想:「既然每个测试用例都创建同样的数据,那我能不能把数据放在夹具的成员变量里,一次创建,多次使用?」
嗯,这里有个陷阱。
警告:夹具的成员变量,在每个 TEST_F 执行前都会被重新创建。SetUp 中初始化的数据,不会跨测试用例共享。这是设计如此,不是 bug。
如果你真的需要跨测试用例共享数据(比如一个只读的配置表),可以用静态成员变量:
class ConfigTest : public ::testing::Test {
protected:
static void SetUpTestSuite() {
// 整个测试套件执行前调用一次
shared_config_ = LoadConfigFromFile("config.json");
}
static void TearDownTestSuite() {
// 整个测试套件执行后调用一次
delete shared_config_;
shared_config_ = nullptr;
}
static Config* shared_config_;
};
Config* ConfigTest::shared_config_ = nullptr;
TEST_F(ConfigTest, CheckTimeout) {
EXPECT_GT(shared_config_->timeout_ms, 0);
}
TEST_F(ConfigTest, CheckRetryCount) {
EXPECT_EQ(shared_config_->retry_count, 3);
}
SetUpTestSuite 和 TearDownTestSuite 是静态方法,在整个测试套件(也就是这个夹具类对应的所有 TEST_F)的生命周期中,只执行一次。适合加载大文件、建立数据库连接等重量级操作。
5.4 避免代码重复的进阶技巧
除了 SetUp/TearDown,还有一些模式可以帮你进一步减少重复:
5.4.1 辅助函数
如果某些操作只在部分测试用例中用到,别硬塞进 SetUp。写个辅助函数,按需调用:
class OrderTest : public ::testing::Test {
protected:
void SetUp() override {
order_ = new Order();
}
void TearDown() override {
delete order_;
}
void AddItems(int count) {
for (int i = 0; i < count; ++i) {
order_->AddItem(Item("item_" + std::to_string(i)));
}
}
Order* order_;
};
TEST_F(OrderTest, EmptyOrder) {
EXPECT_EQ(order_->TotalPrice(), 0);
}
TEST_F(OrderTest, WithItems) {
AddItems(3);
EXPECT_GT(order_->TotalPrice(), 0);
}
5.4.2 参数化测试
当测试逻辑相同,只是输入数据不同时,用参数化测试比写多个 TEST_F 更干净。这个我们后面有专门章节讲,这里先提一嘴。
5.5 避坑指南:我踩过的几个坑
做 TDD 这些年,我在夹具上栽过不少跟头。分享几个典型的:
- 忘记调用父类 SetUp:如果你继承了别人的夹具类,记得在子类的 SetUp 中调用
Base::SetUp()。我曾经漏掉过一次,结果基类初始化的资源全是空的,排查了半天。 - TearDown 中抛异常:TearDown 里别抛异常。Google Test 会捕获它,但后续的清理工作可能不会执行。我建议在 TearDown 中用
EXPECT_*而不是ASSERT_*,前者不会终止流程。 - 静态成员变量线程不安全:如果你用
SetUpTestSuite加载共享数据,并且测试是并发执行的(比如用了--gtest_filter并行跑),记得加锁。虽然 Google Test 默认是串行执行,但有些测试框架会并行化。
5.6 知识体系总览
下面这张图,帮你理清测试夹具的核心脉络:
5.7 小结
测试夹具不是什么高深的技术,但它能极大提升你写测试的效率。我个人习惯是:只要有两个以上的测试用例用到相同的初始化逻辑,就立刻提取成夹具。别等到写了十个重复的 TEST 再重构,那时候改起来就痛苦了。
记住几个要点:
- 用
TEST_F替代TEST,继承::testing::Test - SetUp 做准备,TearDown 做清理,成对出现
- 每个 TEST_F 的数据是隔离的,别指望成员变量跨用例共享
- 需要跨用例共享时,用静态成员 + SetUpTestSuite
- 辅助函数和参数化测试是减少重复的利器
小提示:刚开始用夹具时,你可能会觉得「多写了一个类,好麻烦」。但等你维护了上百个测试用例后,就会明白——当初那点「麻烦」,省下来的可是成倍的时间。