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>类。你要确保它对intdoublestring都能正常工作。难道每个类型写一套测试?

Google Test提供了TYPED_TEST_SUITETYPED_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->会导致依赖名称查找失败。嗯,这个语法细节很容易被忽略。

注意:类型参数化测试不支持SetUpTestSuiteTearDownTestSuite。如果你需要全局初始化,可以考虑用SetUp()配合静态标志位来实现。

18.4 三者对比与选型建议

这三种特性各有各的适用场景。我整理了一个表格,方便你快速决策:

特性 适用场景 宏/关键字 典型例子
测试夹具 多个测试共享相同的环境准备和清理逻辑 TEST_F 数据库测试、网络连接测试
参数化测试 同一个逻辑,不同输入值 TEST_P, INSTANTIATE_TEST_SUITE_P 排序算法、字符串解析
类型参数化测试 同一个逻辑,不同数据类型 TYPED_TEST, TYPED_TEST_SUITE 容器类、泛型算法

我个人习惯是:先看有没有「环境准备」的重复代码,有就用夹具。再看有没有「输入变化」的需求,有就用参数化。最后看是不是「类型变化」,是就用类型参数化。这三个工具组合起来,基本能覆盖90%的测试场景。

18.5 知识体系总览

下面这张图帮你理清这三个特性的关系和适用边界:

Google Test 高级特性知识体系 测试夹具 TEST_F + SetUp/TearDown 共享环境准备与清理 避免重复代码 参数化测试 TEST_P + INSTANTIATE 同一逻辑不同输入 自动生成多个用例 类型参数化测试 TYPED_TEST + TYPED_TEST_SUITE 同一逻辑不同类型 泛型代码验证 选型建议 1. 有环境准备/清理的重复代码? → 用测试夹具 2. 需要测不同输入值? → 用参数化测试 3. 需要测不同数据类型? → 用类型参数化测试 4. 三者可以组合使用:夹具 + 参数化 = TestWithParam 三者互不冲突,可根据实际需求灵活组合

18.6 实战中的避坑指南

最后,分享几个我踩过的坑:

  • 夹具成员必须是protected或publicTEST_F生成的类继承自夹具,private成员访问不到。我刚开始写的时候就把成员设成private,编译报错半天没反应过来。
  • 参数化测试的参数类型必须支持拷贝:因为Google Test内部会复制参数。如果你传了一个unique_ptr,编译直接报错。可以用指针或者shared_ptr绕过去。
  • 类型参数化测试里别用静态局部变量:不同类型实例化出来的测试是独立的,但静态变量是共享的。我曾经在TYPED_TEST里放了一个static计数器,结果不同类型之间互相干扰,查了好久才发现。
  • INSTANTIATE_TEST_SUITE_P的名字不能重复:同一个测试套件里,如果你写了两个INSTANTIATE_TEST_SUITE_P,第二个会覆盖第一个。我建议每个套件只写一个实例化块,把所有参数都放在里面。

好了,这三个高级特性就讲到这里。你可以在自己的项目里试试看,先从测试夹具入手,再慢慢引入参数化测试。相信我,用习惯了就再也回不去了。


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