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 都会:

  1. 创建一个新的夹具对象
  2. 调用 SetUp()
  3. 执行你的测试用例
  4. 调用 TearDown()
  5. 销毁夹具对象

每个测试用例之间,数据是完全隔离的。这一点很重要,我后面会细说。

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);
}

SetUpTestSuiteTearDownTestSuite 是静态方法,在整个测试套件(也就是这个夹具类对应的所有 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 知识体系总览

下面这张图,帮你理清测试夹具的核心脉络:

测试夹具(Test Fixtures)知识体系 测试夹具 生命周期管理 SetUp / TearDown SetUpTestSuite / TearDownTestSuite 数据共享策略 成员变量(每个TEST_F独立) 静态成员(跨TEST_F共享) 代码复用模式 辅助函数 参数化测试 核心目标:减少重复,提高测试可维护性 每个 TEST_F 独立运行,数据隔离,互不干扰 💡 记住:SetUp 和 TearDown 要成对出现

5.7 小结

测试夹具不是什么高深的技术,但它能极大提升你写测试的效率。我个人习惯是:只要有两个以上的测试用例用到相同的初始化逻辑,就立刻提取成夹具。别等到写了十个重复的 TEST 再重构,那时候改起来就痛苦了。

记住几个要点:

  • TEST_F 替代 TEST,继承 ::testing::Test
  • SetUp 做准备,TearDown 做清理,成对出现
  • 每个 TEST_F 的数据是隔离的,别指望成员变量跨用例共享
  • 需要跨用例共享时,用静态成员 + SetUpTestSuite
  • 辅助函数和参数化测试是减少重复的利器

小提示:刚开始用夹具时,你可能会觉得「多写了一个类,好麻烦」。但等你维护了上百个测试用例后,就会明白——当初那点「麻烦」,省下来的可是成倍的时间。


公众号:蓝海资料掘金营,微信deep3321