《C++ 工具库实战》GTest 单元测试:断言、全局环境、局部夹具与底层原理

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux系统从入门到实践》《Linux网络从入门到实践》

《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

[一、gtest 简介与安装](#一、gtest 简介与安装)

[1.1 gtest 介绍](#1.1 gtest 介绍)

[1.2 gtest 核心特性](#1.2 gtest 核心特性)

[1.3 gtest 安装](#1.3 gtest 安装)

[二、gtest 使用介绍](#二、gtest 使用介绍)

[3.1 头文件包含](#3.1 头文件包含)

[3.2 框架初始化接口](#3.2 框架初始化接口)

[3.3 调用测试样例](#3.3 调用测试样例)

[3.4 TEST 宏](#3.4 TEST 宏)

[3.5 断言宏](#3.5 断言宏)

[3.5.1 常用断言宏](#3.5.1 常用断言宏)

[3.5.2 补充:字符串断言(拓展)](#3.5.2 补充:字符串断言(拓展))

[三、gtest 简单使用示例代码](#三、gtest 简单使用示例代码)

[3.1 simple.cc:](#3.1 simple.cc:)

[3.2 Makefile:](#3.2 Makefile:)

[3.3 运行结果展示:ASSERT 与 EXPECT 断言的区别](#3.3 运行结果展示:ASSERT 与 EXPECT 断言的区别)

[3.3.1 场景 1:全部断言正确,测试全部通过](#3.3.1 场景 1:全部断言正确,测试全部通过)

[3.3.2 场景 2:使用 ASSERT_致命断言,断言失败直接终止当前用例](#3.3.2 场景 2:使用 ASSERT_致命断言,断言失败直接终止当前用例)

[3.3.3 场景 3:使用 EXPECT_非致命断言,断言失败继续跑完当前测试用例](#3.3.3 场景 3:使用 EXPECT_非致命断言,断言失败继续跑完当前测试用例)

[3.4 ASSERT 和 EXPECT 对比小结](#3.4 ASSERT 和 EXPECT 对比小结)

[四、GTest 事件机制](#四、GTest 事件机制)

[4.1 测试流程基础](#4.1 测试流程基础)

[4.2 事件机制层级结构](#4.2 事件机制层级结构)

[4.3 GTest 三类事件分类](#4.3 GTest 三类事件分类)

[4.4 事件机制的价值](#4.4 事件机制的价值)

[五、GTest 全局测试环境完整案例](#五、GTest 全局测试环境完整案例)

[5.1 方案一:全局容器变量写法(缺陷)](#5.1 方案一:全局容器变量写法(缺陷))

[5.2 方案二:推荐写法(数据封装 + 全局指针作为访问入口)](#5.2 方案二:推荐写法(数据封装 + 全局指针作为访问入口))

[5.3 GTest 全局测试环境坑点](#5.3 GTest 全局测试环境坑点)

[5.4 两种实现方案对比](#5.4 两种实现方案对比)

[5.5 全局测试环境遗留问题](#5.5 全局测试环境遗留问题)

[六、GTest 局部测试夹具(局部测试环境)完整案例](#六、GTest 局部测试夹具(局部测试环境)完整案例)

[6.1 单条用例独立夹具代码案例](#6.1 单条用例独立夹具代码案例)

[6.2 套件级静态钩子代码案例](#6.2 套件级静态钩子代码案例)

[6.3 TEST_F 核心机制:数据隔离与共享原理](#6.3 TEST_F 核心机制:数据隔离与共享原理)

[6.3.1 测试套件的绑定机制](#6.3.1 测试套件的绑定机制)

[6.3.2 数据隔离的实现:实例级生命周期](#6.3.2 数据隔离的实现:实例级生命周期)

[6.3.3 数据共享的实现:类级别静态机制](#6.3.3 数据共享的实现:类级别静态机制)

[6.3.4 核心注意事项](#6.3.4 核心注意事项)

七、三类钩子机制对比

[八、GTest 面试总结要点](#八、GTest 面试总结要点)

结束语


前言

在 C++ 项目开发过程中,随着业务代码体量不断增长,单纯依靠人工测试很难保证代码模块的正确性,回归测试的成本也会急剧上升。单元测试就是用来解决这个问题的有效手段,而 GTest 作为谷歌开源的 C++ 单元测试框架,在工业级项目中被广泛使用。

在前序文章《C++ 工具库实战》gflags 命令行参数解析库详解中,我们学习了如何通过命令行参数实现程序运行期配置,无需反复修改代码重新编译。本篇将继续 C++ 工具库实战系列,完整讲解 GTest 单元测试框架。

本文会从框架的安装、基础宏、断言开始,由浅入深,逐步介绍 GTest 的事件机制,包括全局测试环境、局部测试夹具。我们会结合完整代码案例,剖析 TEST_F 夹具的数据隔离与数据共享底层原理,区分实例钩子、套件静态钩子、全局环境三类钩子的差异,最后梳理面试核心要点。

一、gtest 简介与安装

1.1 gtest 介绍

GTest (Google Test )是谷歌开源、跨平台的 C++ 单元测试框架。它可以帮助开发者为 C++ 代码编写自动化单元测试,不用手动编写大量判断与打印逻辑。

框架内置大量断言宏,支持致命 / 非致命性校验、参数化测试、测试套件分组等能力,能够批量执行多组测试用例,自动统计成功与失败的用例,方便快速定位代码缺陷,广泛用于 C++ 后端、底层库的自动化验证。

1.2 gtest 核心特性

gtest具备下面这些核心特性:

  • **跨平台支持:**可在 Linux、Windows、macOS 等主流操作系统运行,适配不同编译器。
  • 丰富的断言体系: 分为致命断言 与非致命断言。致命断言失败会直接终止当前测试用例;非致命断言失败仅记录错误,继续执行后续代码,一次运行可以一次性收集多处问题。
  • **测试分组管理:**支持测试套件(TestSuite)+ 测试用例(Test)两级组织,可对多个相关测试用例归类,按需执行某一组测试。
  • **参数化测试:**同一套测试逻辑,批量传入多组不同输入参数,减少重复代码,覆盖更多边界场景。
  • **测试夹具(TestFixture):**提供统一的前置初始化与后置清理机制,多个测试用例复用同一套环境资源,避免重复写初始化代码。
  • **简洁的执行与报告:**自动运行所有用例,输出清晰的测试统计信息,包含失败位置、预期值与实际值对比,方便快速排错。
  • **支持自定义事件监听:**可以扩展测试执行前后的回调,自定义日志输出、测试结果上报等逻辑。

1.3 gtest 安装

Ubuntu / Debian:

bash 复制代码
sudo apt-get install libgtest-dev

CentOS(CentOS7 / CentOS8):

bash 复制代码
# CentOS7
sudo yum install gtest-devel

# CentOS8
sudo dnf install gtest-devel

安装完成结果:

二、gtest 使用介绍

3.1 头文件包含

使用 GTest 库,首先引入核心头文件,几乎所有测试代码都需要它:

cpp 复制代码
#include <gtest/gtest.h>

3.2 框架初始化接口

在main函数中调用InitGoogleTest,用于解析命令行参数、完成 GTest 框架初始化 ,接收argc与argv地址。

cpp 复制代码
testing::InitGoogleTest(&argc, argv);

3.3 调用测试样例

RUN_ALL_TESTS() 是框架提供的宏,会自动扫描所有TEST/TEST_F定义的测试用例,批量执行全部测试,返回测试结果。

cpp 复制代码
RUN_ALL_TESTS();

3.4 TEST 宏

cpp 复制代码
// 宏参数不需要双引号,同一个测试套件内,测试样例名称不能重复
TEST(测试套件名称, 测试样例名称)
TEST_F(test_fixture, test_name)

// 举例:
TEST(vector_test, insert)
{
    //...
}

// 错误写法
TEST("vector_test", insert)
  • TEST:用于编写独立简单测试。本质是自动生成测试函数,在函数内部可以编写 C++ 业务代码,并搭配断言宏做结果校验,适合无共享资源的独立测试场景。
  • TEST_F:带测试夹具的测试,适合一组测试用例需要复用同一套初始化资源的场景。可以在测试执行前统一创建对象、申请内存,测试结束后统一释放资源,使用同一套数据,验证不同业务逻辑。

3.5 断言宏

GTest 断言宏分为两大类别,二者的失败行为差异是使用时最关键的区分点:

  • ASSERT_ 系列(致命断言): 一旦断言失败,直接终止当前这个测试用例 ,同用例后续代码不再执行。适合严重错误,无法继续后面校验的场景。
  • EXPECT_ 系列(非致命断言): 断言失败只会记录错误信息,不会中断当前测试函数,会继续向下执行其余断言,一次运行可以收集多处 bug,日常开发更推荐优先使用。

3.5.1 常用断言宏

cpp 复制代码
// bool布尔值检查
ASSERT_TRUE(参数);    // 预期表达式结果为true
ASSERT_FALSE(参数);   // 预期表达式结果为false

// 数值比较
ASSERT_EQ(参数1, 参数2); // equal,两值相等
ASSERT_NE(参数1, 参数2); // not equal,两值不相等
ASSERT_LT(参数1, 参数2); // less than,参数1 < 参数2
ASSERT_GT(参数1, 参数2); // greater than,参数1 > 参数2
ASSERT_LE(参数1, 参数2); // less equal,参数1 <= 参数2
ASSERT_GE(参数1, 参数2); // greater equal,参数1 >= 参数2

对应 EXPECT 版本:EXPECT_TRUE / EXPECT_FALSE / EXPECT_EQ / EXPECT_NE / EXPECT_LT / EXPECT_GT / EXPECT_LE / EXPECT_GE ,用法完全一致,仅失败后的行为不同。

3.5.2 补充:字符串断言(拓展)

cpp 复制代码
ASSERT_STREQ(str1, str2);   // C风格字符串相等比较
ASSERT_STRNE(str1, str2);   // C风格字符串不相等
// EXPECT_STREQ / EXPECT_STRNE 为非致命版本

注意:ASSERT_EQ 可以用于 std::string ,但原始 C 字符串 char* 推荐使用ASSERT_STREQ,避免直接比较指针地址造成判断错误。

三、gtest 简单使用示例代码

3.1 simple.cc:

cpp 复制代码
/*
    gtest简单使用实例:
    1、包含头文件 <gtest/gtest.h>
    2、定义测试用例
    3、初始化测试环境
    4、执行测试用例
*/

#include <iostream>
#include <unordered_map>
#include <gtest/gtest.h> // 引入gtest框架头文件

// TEST(测试套件名, 测试用例名)
// 测试套件:unordered_map_test,用于归类所有和unordered_map相关的测试
// 测试用例:insert,专门测试插入功能
TEST(unordered_map_test, insert)
{
    // 创建一个无序哈希表容器
    std::unordered_map<int, int> map;
    map.insert(std::make_pair(1, 1));
    map.insert(std::make_pair(2, 2));
    map.insert(std::make_pair(3, 3));

    // 致命断言:判断容器元素数量是否等于3
    // 如果不等于3,断言失败,直接终止"当前"这个insert测试用例,即使后续仍存在断言宏判断也不会执行了
    ASSERT_EQ(map.size(), 3);

    // ASSERT_EQ(map.size(), 2);
}

// 同一测试套件 unordered_map_test 下的第二个测试用例 find
// 用来测试unordered_map的查找功能
TEST(unordered_map_test, find)
{
    std::unordered_map<int, int> map;
    map.insert(std::make_pair(1, 1));
    map.insert(std::make_pair(2, 2));
    map.insert(std::make_pair(3, 3));
    // 断言:查找key=1的迭代器不等于end(),代表key存在
    ASSERT_NE(map.find(1), map.end());
    // 断言:key=1对应的value值等于1
    ASSERT_EQ(map.find(1)->second, 1);

    // 下面是用于演示失败场景
    // ASSERT_EQ(map.find(1), map.end()); // 断言:key=1等于end,会直接失败,终止当前用例
    // EXPECT_EQ(map.find(1), map.end()); // 非致命断言,失败仅记录错误,继续执行后续代码
    // EXPECT_EQ(map.find(1)->second, 10);
}
int main(int argc, char *argv[])
{
    // 初始化gtest框架,解析命令行参数
    testing::InitGoogleTest(&argc, argv);
    // 自动运行所有TEST定义的测试用例,返回测试结果
    return RUN_ALL_TESTS();
}

3.2 Makefile:

cpp 复制代码
simple:simple.cc
	g++ -o $@ $^ -lgtest

.PHONY:clean
clean:
	rm -rf simple

代码整体分析:

  1. 测试套件与用例划分 TEST(unordered_map_test, insert) 和 TEST(unordered_map_test, find) 属于同一个测试套件unordered_map_test ,代表都是针对unordered_map容器的测试;两个用例分别独立运行,互不干扰。

每一个 TEST 宏 会生成独立的测试函数 ,两个用例内部的**map变量** 是完全独立的对象 ,互不共享。

  1. 断言选择说明 这里使用ASSERT_系列致命断言:一旦校验失败,当前测试用例立刻停止。
  • ASSERT_EQ(a,b):a == b 才通过
  • ASSERT_NE(a,b):a != b 才通过
  1. 测试逻辑
  • **insert 用例:**验证插入 3 个元素后,容器 size 为 3,检验插入功能是否生效。
  • **find 用例:**验证 key=1 能够被找到,并且取出的 value 和预期一致。
  1. main 函数作用 不需要手动调用各个 TEST 函数。RUN_ALL_TESTS()会自动扫描全部 TEST 宏定义的测试用例,自动依次执行,并且输出测试报告,统计成功 / 失败数量。

编译命令:

bash 复制代码
g++ test.cpp -o test -lgtest -lpthread

gtest 依赖 pthread 库,编译必须加上-lpthread。

运行:

bash 复制代码
./test

程序会自动执行unordered_map_test下的 insert、find 两个用例,打印每个用例是否成功。

3.3 运行结果展示:ASSERT 与 EXPECT 断言的区别

3.3.1 场景 1:全部断言正确,测试全部通过

代码中ASSERT_EQ(map.size(),3)、ASSERT_NE、ASSERT_EQ全部满足预期。

运行输出:两个测试用例全部执行,最终PASSED 2 tests。

  • GTest 会自动运行unordered_map_test套件下所有 TEST 用例,两个用例互相独立,每个 TEST 都会单独创建销毁环境。

3.3.2 场景 2:使用 ASSERT_致命断言,断言失败直接终止当前用例

运行结果解读:

  1. unordered_map_test.insert:ASSERT_EQ校验失败,立刻结束 insert 这个测试用例。
  2. unordered_map_test.find:ASSERT_EQ断言失败,函数直接 return,后面的EXPECT_EQ不会执行。

核心特点: ASSERT_*系列属于致命断言 ,一旦失败,直接退出当前 TEST 函数,同用例后续代码不再运行;但是不会影响其他测试用例,insert 失败后 find 依旧会被执行。

3.3.3 场景 3:使用 EXPECT_非致命断言,断言失败继续跑完当前测试用例

运行结果解读:

  • EXPECT_*属于非致命断言 ,断言失败仅记录错误信息,不会终止当前测试函数,同用例后续代码会继续执行。
  • 所以 find 用例中,两条 EXPECT 都会被校验,控制台会打印两条 Failure 失败日志。

3.4 ASSERT 和 EXPECT 对比小结

  • **ASSERT_XX:致命断言。**失败 → 终止当前 TEST 用例,后续代码不跑。适合:后续代码依赖本次断言结果,继续执行会引发崩溃(比如空指针解引用)。
  • **EXPECT_XX:非致命断言。**失败 → 仅记录错误,继续执行当前 TEST 剩余代码。适合:希望一次性收集当前用例全部问题,一次性看到所有 bug。

注意:不同 TEST 测试用例之间永远相互独立,无论 ASSERT 还是 EXPECT,一个用例失败,不会阻止其他测试用例执行。

四、GTest 事件机制

事件机制是 GTest 的核心能力之一,也是 GTest 相比单纯手动写日志打印做测试更方便好用的关键原因。如果不使用事件机制,我们只能靠手动增加日志、在代码里写 if 判断来校验结果,环境准备与资源清理代码会大量重复,测试用例之间还容易互相干扰。而事件机制可以帮我们在测试执行的不同阶段,自动执行环境初始化、数据准备与资源清理逻辑。

4.1 测试流程基础

一次完整的测试执行 分为三个核心步骤:

  1. **初始测试环境:**提前准备测试所需数据、申请资源
  2. 执行各个测试用例,完成业务逻辑校验
  3. **清理测试环境:**销毁临时数据、释放申请的资源

测试环境的实现思路分为两类:

  • **全局测试环境初始化:**多个 TEST 测试用例共用同一份环境,缺点是用例之间会相互影响,前一个用例修改的数据会污染后续用例
  • **局部测试环境初始化:**每个 TEST 用例拥有独立的测试环境,用例之间互不干扰

4.2 事件机制层级结构

GTest 事件机制 ,就是在测试执行前后,提供留给用户自定义 执行逻辑的回调机制 ,同时支持同一测试套件内的用例共享数据。事件按照执行层级分为三级,层级从大到小:

  1. 测试程序级 :整个可执行测试程序,全局仅一份。事件在整个程序启动前、程序结束后执行。
  2. 测试套件级 :一个套件是一组相关测试用例的集合。事件在该套件第一个用例执行前、套件所有用例全部跑完之后执行。
  3. 测试用例级 :每一个 TEST 测试用例。事件在单个用例运行之前、单个用例运行结束之后执行。

4.3 GTest 三类事件分类

  • **全局事件:**作用于整个测试程序,程序启动时执行一次初始化,程序退出前执行清理。
  • **套件事件:**作用于单个测试套件,套件内所有用例执行前初始化,套件全部用例跑完后清理。
  • 用例事件: 作用于每一条独立测试用例,每一个用例执行前初始化,每一个用例结束后清理,也就是 TEST_F 测试夹具。

4.4 事件机制的价值

事件机制的核心作用,就是自动化管理测试环境 。

在执行测试用例之前自动完成环境、数据准备 ;测试结束之后自动清理本次测试产生的临时数据与资源 。

当同一段业务代码需要编写多组测试用例时,借助事件机制,我们可以在每个用例执行前自动初始化环境,用例结束后自动消除本次测试带来的状态改动,保证不同测试用例之间互不污染。

五、GTest 全局测试环境完整案例

5.1 方案一:全局容器变量写法(缺陷)

cpp 复制代码
/*
    gtest全局测试套件使用:
    1、包含头文件 <gtest/gtest.h>
    2、定义全局测试环境类
      2.1 定义测试环境初始接口,会在所有用例执行之前执行
      2.2 定义测试环境清理接口,会在所有用例执行之后执行
    3、定义测试用例
    4、初始化测试1用例
    5、执行测试用例
*/

#include <iostream>
#include <unordered_map>
#include <gtest/gtest.h>

// ===================== 方案1:简单demo方案(全局变量) =====================
// 全局容器,所有测试用例共享这份数据
std::unordered_map<int, int> map;

// 全局测试环境类,继承 testing::Environment
class GlobalTestEnviroment : public testing::Environment
{
public:
    // 全局初始化钩子:在全部TEST用例运行前,只会执行一次
    void SetUp() override
    {
        std::cout << "===========执行于所有用例之前===========" << std::endl;
        std::cout << "向map添加数据..." << std::endl;
        map.insert(std::make_pair(1, 1));
        map.insert(std::make_pair(2, 2));
        map.insert(std::make_pair(3, 3));
        std::cout << "添加数据完成" << std::endl;
    }

    // 全局清理钩子:全部TEST用例执行完毕后,只会执行一次
    void TearDown() override
    {
        std::cout << "清除map所有数据" << std::endl;
        map.clear();
        std::cout << "===========执行于所有用例之后===========" << std::endl;
    }
};

// 测试用例1:删除key=1
TEST(GlobalTestEnviroment, remove)
{
    map.erase(1);
}

// 测试用例2:校验map元素数量
TEST(GlobalTestEnviroment, insert)
{
    // ASSERT_EQ(map.size(), 3);
    ASSERT_EQ(map.size(), 2);
}

// 测试用例3:查找已经被删除的key=1
TEST(GlobalTestEnviroment, find)
{
    ASSERT_EQ(map.find(1), map.end());
}

int main(int argc, char *argv[])
{
    // 初始化gtest框架
    testing::InitGoogleTest(&argc, argv);
    // 注册全局环境对象,gtest会自动管理这个对象生命周期
    testing::AddGlobalTestEnvironment(new GlobalTestEnviroment);
    // 运行所有测试用例
    return RUN_ALL_TESTS();
}

方案一代码分析:

  • map定义在全局域,整个文件的所有函数、TEST 用例、Environment 类内部都可以直接访问。
  • GlobalTestEnviroment 负责全局数据 的初始化与清理 ,SetUp 在所有用例运行前填充 map ,TearDown 在全部用例结束后清空 map。
  • TEST 套件名GlobalTestEnviroment只是一个名字,TEST 不会继承这个类,只是共用全局变量 map。
  • 特点:
    • **优点:**代码简洁,上手简单,适合学习演示。
    • 缺点: 全局变量耦合度高,容易命名冲突;所有用例共用同一份 map,前面用例修改数据会污染后面的用例,测试不具备隔离性(不具备隔离性并不是缺点而是特点,因为存在一些情况就是需要我们在多个测试案例对全局数据进行多次修改操作)。

5.2 方案二:推荐写法(数据封装 + 全局指针作为访问入口)

cpp 复制代码
/*
    gtest全局测试套件使用:
    1、包含头文件 <gtest/gtest.h>
    2、定义全局测试环境类
      2.1 定义测试环境初始接口,会在所有用例执行之前执行
      2.2 定义测试环境清理接口,会在所有用例执行之后执行
    3、定义测试用例
    4、初始化测试1用例
    5、执行测试用例
*/

#include <iostream>
#include <unordered_map>
#include <gtest/gtest.h>
// ===================== 方案2:工程推荐写法(封装+getter,无全局容器) =====================

// 全局测试环境类,继承 testing::Environment
class GlobalTestEnviroment : public testing::Environment
{
public:
    // 全局初始化钩子:在全部TEST用例运行前,只会执行一次
    void SetUp() override
    {
        std::cout << "===========执行于所有用例之前===========" << std::endl;
        std::cout << "向map添加数据..." << std::endl;
        _map.insert(std::make_pair(1, 1));
        _map.insert(std::make_pair(2, 2));
        _map.insert(std::make_pair(3, 3));
        std::cout << "添加数据完成" << std::endl;
    }

    // 全局清理钩子:全部TEST用例执行完毕后,只会执行一次
    void TearDown() override
    {
        std::cout << "清除map所有数据" << std::endl;
        _map.clear();
        std::cout << "===========执行于所有用例之后===========" << std::endl;
    }

    // 对外提供获取容器的接口,控制访问入口
    std::unordered_map<int, int> &GetMap()
    {
        return _map;
    }

private:
    // map作为类私有成员,不再是全局变量,实现封装
    std::unordered_map<int, int> _map;
};

// 保存全局环境实例指针,供测试用例访问
GlobalTestEnviroment *g_env_ptr = nullptr;

// 测试用例1:删除key=1
TEST(GlobalTestEnviroment, remove)
{
    g_env_ptr->GetMap().erase(1);
}

// 测试用例2:校验map元素数量
TEST(GlobalTestEnviroment, insert)
{
    // ASSERT_EQ(map.size(), 3);
    ASSERT_EQ(g_env_ptr->GetMap().size(), 2);
}

// 测试用例3:查找已经被删除的key=1
TEST(GlobalTestEnviroment, find)
{
    auto &map = g_env_ptr->GetMap();
    ASSERT_EQ(map.find(1), map.end());
}

int main(int argc, char *argv[])
{
    // 初始化gtest框架
    testing::InitGoogleTest(&argc, argv);
    // 创建全局环境对象,保存实例地址到全局指针
    g_env_ptr = new GlobalTestEnviroment;
    testing::AddGlobalTestEnvironment(g_env_ptr);

    // 运行所有测试用例
    return RUN_ALL_TESTS();
    // AddGlobalTestEnvironment的特性:gtest 会接管这个对象的生命周期,TearDown 完毕自动 delete
    // 所以我们无需手动 delete g_env_ptr
}

方案二代码分析:

  • 数据_map作为GlobalTestEnviroment的私有成员,被封装在实例内部,不再是全局变量,外部不能直接访问。
  • 提供GetMap()公有成员函数,作为唯一对外访问通道,控制数据读写,符合面向对象封装原则。
  • g_env_ptr是全局指针,存放 Environment 唯一实例的地址。TEST 函数通过这个全局指针拿到对象,调用GetMap()拿到 map 引用。
  • 关键点: gtest 的SetUp/TearDown回调本身不需要全局指针;全局指针只是为了让 TEST 能够找到这个单实例。
  • 特点:
    • **优点:**数据封装,不会随意暴露,工程上更规范。
    • 缺点:仍然只有一个 Environment 实例 ,多个 TEST 用例依旧共享同一份_map,用例之间数据会相互污染,无法做到用例隔离。

5.3 GTest 全局测试环境坑点

很多初学者会想:把 map 放到 GlobalTestEnviroment 类内部作为 protected 成员,直接在 TEST 里面访问。这是严重的错误写法!

cpp 复制代码
class GlobalTestEnviroment : public testing::Environment
{
public:
    void SetUp() override
    {
        // ...往成员map插入数据
    }
protected:
    std::unordered_map<int, int> map; // 放在类内protected
};

TEST(GlobalTestEnviroment, remove)
{
    map.erase(1); // 编译报错:'map' was not declared in this scope
}

坑点原理分析:

TEST 生成的测试类 完全不继承 GlobalTestEnviroment ,二者没有继承关系 。

哪怕成员写成 protected,TEST 里面也无法直接访问。

protected 访问权限,只对子类 开放,TEST 并不是它的子类,因此访问失败。private 同理。

这也是接下来要讲解的为什么 TEST_F 能够使用 GlobalTestEnviroment 中的protected成员变量的原理埋下铺垫。

5.4 两种实现方案对比

  • **方案 1(直接全局 map):**编码最简单,适合学习 demo;缺点:全局变量,耦合高,容易命名冲突,全部用例共享全局状态,用例之间相互污染。
  • **方案 2(指针 + getter):**工程推荐;数据封装在环境类内,符合面向对象封装思想,仅暴露访问接口。

5.5 全局测试环境遗留问题

即便使用 指针 + getter 的写法,多个 TEST 用例之间依旧共享同一份 map 数据 。

测试用例执行存在先后顺序,前面用例修改 map,后面用例的执行结果会被影响。

解决思路: 如果希望每条用例数据独立 ,就不要使用testing::Environment,改用**TEST_F局部夹具** ,每条用例单独构造销毁,实现用例隔离。

六、GTest 局部测试夹具(局部测试环境)完整案例

6.1 单条用例独立夹具代码案例

cpp 复制代码
/*
    gtest局部测试套件使用:
    1、包含头文件 <gtest/gtest.h>
    2、定义局部测试环境类
      2.1 定义测试环境初始接口,会在每一个测试用例执行之前执行
      2.2 定义测试环境清理接口,会在每一个测试用例执行之后执行
    3、定义测试用例(局部测试套件无需像全局测试套件那样调用AddGlobalTestEnvironment初始化)
    4、执行测试用例
*/

#include <iostream>
#include <unordered_map>
#include <gtest/gtest.h>

// ===================== 单条用例独立夹具(仅SetUp/TearDown) =====================

// 局部测试夹具类,必须继承 testing::Test
class LocalTestSuite : public testing::Test
{
public:
    // 每条TEST_F用例运行前,都会执行一次SetUp
    void SetUp() override
    {
        std::cout << "===========执行于每一个用例之前===========" << std::endl;
        std::cout << "向map添加数据..." << std::endl;
        _map.insert(std::make_pair(1, 1));
        _map.insert(std::make_pair(2, 2));
        _map.insert(std::make_pair(3, 3));
        std::cout << "添加数据完成" << std::endl;
    }

    // 每条TEST_F用例运行结束后,都会执行一次TearDown
    void TearDown() override
    {
        std::cout << "清除map所有数据" << std::endl;
        _map.clear();
        std::cout << "===========执行于每一个用例之后===========" << std::endl;
    }

    // private:
protected:
    // protected:TEST_F自动生成的测试子类继承本夹具,可以直接访问_map
    // 如果写成private,子类无法访问,编译报错
    std::unordered_map<int, int> _map;
};

// 测试用例1:查找key=1,校验value
TEST_F(LocalTestSuite, find)
{
    ASSERT_NE(_map.find(1), _map.end());
    ASSERT_EQ(_map.find(1)->second, 1);
}

// 测试用例2:删除key=1
TEST_F(LocalTestSuite, remove)
{
    _map.erase(1);
}

// 测试用例3:校验map元素数量
TEST_F(LocalTestSuite, size)
{
    // 哪个断言失败呢??第二个
    // 重点:每一条 TEST_F 都会新建独立夹具对象,单独执行SetUp,map重新被初始化insert了3个元素
    // 第一个断言 ASSERT_EQ(_map.size(), 3) 成立,第二个断言失败
    ASSERT_EQ(_map.size(), 3);
    ASSERT_EQ(_map.size(), 2);
}

int main(int argc, char *argv[])
{
    // 初始化gtest框架
    testing::InitGoogleTest(&argc, argv);
    // 运行所有TEST_F测试用例
    return RUN_ALL_TESTS();
}

运行结果展示:

代码原理分析:

1. TEST_F 和 TEST 的本质区别

  • TEST:单纯测试函数,没有配套夹具对象,不会自动执行SetUp/TearDown。
  • TEST_F(夹具类名,用例名):F = Fixture(夹具) 。gtest 会自动生成一个继承LocalTestSuite的子类。 每执行一条TEST_F用例,完整流程:
    • 创建全新的夹具实例
    • 调用SetUp()初始化资源
    • 执行当前测试用例代码
    • 调用TearDown()清理资源
    • 销毁这个夹具对象

核心特性:每条测试用例,对象独立,数据隔离! 上一个用例对_map的修改,完全不会污染下一条测试用例。 和前面testing::Environment全局事件形成鲜明对比:全局 Environment 全程只有 1 个实例,所有用例共享同一份数据。

但是这并不代表局部测试环境中测试案例就没有能够共享的资源!SetUpTestSuite 和 TearDownTestSuite 这两个静态成员函数的作用就是于此!后面通过代码进行讲解。

2. 为什么成员变量放在 protected 而不是 private?

TEST_F 宏 生成的测试类 会公有继承 LocalTestSuite。

  • protected成员:子类可以直接访问;
  • private成员:只有本类内部可见,子类无法访问,编译报错。

重点: remove用例里面删除 key=1,完全不会影响 size 这条用例 ,因为二者是完全独立的两个夹具实例。

6.2 套件级静态钩子代码案例

cpp 复制代码
#include <iostream>
#include <unordered_map>
#include <gtest/gtest.h>

// ===================== 套件级静态钩子 SetUpTestSuite / TearDownTestSuite ====================
/*
    前面SetUp/TearDown是【每条用例前后执行】;
    SetUpTestSuite / TearDownTestSuite 属于套件静态钩子,
    在【当前套件所有用例执行之前仅运行一次】、【当前套件所有用例全部跑完之后仅运行一次】
    搭配静态成员变量,实现套件级别共享数据。
*/

// 局部测试夹具类,必须继承 testing::Test
class LocalTestSuite : public testing::Test
{
public:
    // 【用例级钩子】每一条TEST_F用例运行前,都会执行一次
    void SetUp() override
    {
        std::cout << "===========执行于每一个用例之前===========" << std::endl;
        std::cout << "向map添加数据..." << std::endl;
        _map.insert(std::make_pair(1, 1));
        _map.insert(std::make_pair(2, 2));
        _map.insert(std::make_pair(3, 3));
        std::cout << "添加数据完成" << std::endl;
    }

    // 【用例级钩子】每一条TEST_F用例运行结束后,都会执行一次
    void TearDown() override
    {
        std::cout << "清除map所有数据" << std::endl;
        _map.clear();
        std::cout << "===========执行于每一个用例之后===========" << std::endl;
    }

    // 【套件级静态前置钩子】static,整个LocalTestSuite套件运行前,只执行一次
    static void SetUpTestSuite()
    {
        std::cout << "===========执行于LocalTestSuite套件之前===========" << std::endl;
        std::cout << "向static_map添加数据..." << std::endl;
        _static_map.insert(std::make_pair(1, 1));
        _static_map.insert(std::make_pair(2, 2));
        _static_map.insert(std::make_pair(3, 3));
        std::cout << "添加数据完成" << std::endl;
    }

    // 【套件级静态后置钩子】static,整个LocalTestSuite全部用例跑完,只执行一次
    static void TearDownTestSuite()
    {
        std::cout << "清除static_map所有数据" << std::endl;
        _static_map.clear();
        std::cout << "===========执行于LocalTestSuite套件之后===========" << std::endl;
    }

protected:
    // 普通成员变量:属于单个夹具实例,每条TEST_F都新建对象,数据隔离
    std::unordered_map<int, int> _map;

    // static静态成员变量:属于整个类,套件内所有TEST_F用例共享同一份数据
    static std::unordered_map<int, int> _static_map;
};

// 静态成员类外初始化,必须写在类外部,否则链接报错
std::unordered_map<int, int> LocalTestSuite::_static_map;

// 测试用例1:查找普通成员_map中的key=1,校验value
TEST_F(LocalTestSuite, find)
{
    ASSERT_NE(_map.find(1), _map.end());
    ASSERT_EQ(_map.find(1)->second, 1);
}

// 测试用例2:普通map删除key=1,静态map也删除key=1
TEST_F(LocalTestSuite, remove)
{
// 当前用例独立实例的普通成员,只影响本条用例
    _map.erase(1);
    // 静态成员属于类,修改会影响套件内后续所有用例
    LocalTestSuite::_static_map.erase(1);
}

// 测试用例3:校验 _map元素数量
TEST_F(LocalTestSuite, _map_size)
{
    // 哪个断言失败呢??第二个
    // 重点:每一条 TEST_F 都会新建独立夹具对象,单独执行SetUp,map重新被初始化insert了3个元素
    // 第一个断言 ASSERT_EQ(_map.size(), 3) 成立,第二个断言失败
    ASSERT_EQ(_map.size(), 3);
    ASSERT_EQ(_map.size(), 2);
}

// 测试用例4:校验静态共享_static_map元素数量
TEST_F(LocalTestSuite, _static_map_size)
{
    // 哪个断言失败呢??第一个
    // 前面remove用例已经删除_static_map的key=1,静态数据会被污染
    ASSERT_EQ(LocalTestSuite::_static_map.size(), 3);
    ASSERT_EQ(LocalTestSuite::_static_map.size(), 2);
}

int main(int argc, char *argv[])
{
    // 初始化gtest框架
    testing::InitGoogleTest(&argc, argv);
    // 运行所有TEST_F测试用例
    return RUN_ALL_TESTS();
}

运行结果展示:

代码分析:

1. 两套钩子的层级区分

  • SetUp() / TearDown():实例级(单条用例) 每条 TEST_F 都会新建夹具对象,执行 SetUp;用例结束执行 TearDown 销毁对象。普通成员_map归属于单个实例,用例之间完全隔离,互不干扰。
  • SetUpTestSuite() / TearDownTestSuite():套件级(整个测试套件) 必须标记static。在当前套件全部用例开始前执行一次;套件全部用例跑完后执行一次。适合套件共用资源的初始化,比如数据库连接。

2. 普通成员 _map VS 静态成员 _static_map

  • _map: 非 static,每个 TEST_F 夹具实例独有。
    remove用例中 erase (1) 只会影响当前这条用例,后续 _map_size 用例拿到的 _map 依旧是 3 个元素。
  • _static_map: static 静态成员,不属于任何一个实例,属于LocalTestSuite类本身。
    所有 TEST_F 用例共享同一份静态容器。remove 里面删除 key=1 之后,后面_static_map_size 读取时,_static_map 只剩下 2 个元素,数据会被前面用例污染。

6.3 TEST_F 核心机制:数据隔离与共享原理

在 Google Test 框架中,测试夹具 (Test Fixture)通过巧妙结合 C++ 面向对象特性 与框架的生命周期管理 ,完美实现了**"测试用例间的状态隔离"** 与 "测试套件内的资源共享" 。

这两者看似矛盾,实则通过不同的作用域和生命周期机制被统一在一起。

6.3.1 测试套件的绑定机制

TEST_F 宏 的第一个参数 必须是一个继承自 ::testing::Test 的夹具类名称 。

GTest 正是通过这一参数,将所有使用该夹具类的 TEST_F 测试用例自动划分并绑定 到同一个测试套件(Test Suite)中。

6.3.2 数据隔离的实现:实例级生命周期

"数据隔离" 是 GTest 的核心原则,旨在确保每个测试用例的原子性和独立性,防止测试间的意外耦合。其底层实现依赖于实例级别的生命周期管理:

  • 独立对象创建: 对于每一个 TEST_F 测试用例,GTest 在运行时都会创建一个全新的夹具类对象。
  • 完整生命周期: 每个测试用例都会经历完整的生命周期:构造夹具对象 -> 调用 SetUp() 进行初始化 -> 执行测试逻辑 -> 调用 TearDown() 进行清理 -> 析构并删除夹具对象。
  • 状态隔离效果: 夹具类中定义的普通成员变量属于具体的夹具对象。由于每个测试用例都有自己专属的对象,对普通成员变量的任何修改都会随着该夹具对象的销毁而消失,绝对不会 "污染" 或影响下一个测试。

6.3.3 数据共享的实现:类级别静态机制

"数据共享" 是为了应对某些资源初始化成本过高(如数据库连接、加载大型配置文件)的问题,避免每个测试都重复执行这些耗时操作。其底层实现依赖于C++ 的类级别静态机制:

  • 静态成员变量: 在测试夹具类中,将需要共享的数据 声明为**static(静态)成员变量** 。静态成员变量在内存中只有一份,它属于类本身,而不属于任何一个具体的对象。当多个测试用例(多个对象)去访问该变量时,访问的实际上是内存中的同一个地址。
  • 类级别钩子函数: 通过 static void SetUpTestSuite() 和 static void TearDownTestSuite() 这两个钩子函数来进行共享资源的初始化和清理 。
    SetUpTestSuite() 会在该测试套件下的第一个测试执行前被调用一次;TearDownTestSuite() 会在最后一个测试执行后被调用一次。

6.3.4 核心注意事项

虽然 GTest 允许在测试套件级别共享数据,但在实际开发中需遵循以下规范:

  • **只读原则:**共享数据通常只用于存放只读资源(如只读的数据库连接池、配置文件等)。
  • **避免隐式依赖:**测试代码不应依赖于共享资源的状态,或者在修改后必须将其恢复原状。因为 GTest 并不保证同一套件内测试用例的执行顺序,如果多个测试随意修改共享状态,会导致测试之间产生隐式依赖,从而破坏测试的独立性。

七、三类钩子机制对比

项目 SetUp() / TearDown()实例级钩子 SetUpTestSuite() / TearDownTestSuite()套件静态钩子 testing::Environment全局环境钩子
归属层级 夹具实例(对象级别) 测试套件(类级别,static) 整个测试程序(全局)
执行次数 每一条 TEST_F 用例执行前后,都会调用一次 当前套件全部用例之前执行一次,全部用例结束后执行一次 程序内所有测试套件运行前执行一次,全部套件跑完后执行一次
可访问变量 普通成员变量 + static 静态成员变量 仅能访问 static 静态成员,无法访问实例普通成员 全局变量,独立于夹具类
数据隔离 天然隔离,每个用例独立对象,修改互不污染 套件内所有用例共享同一份 static 数据,存在状态污染风险 全局共享,所有套件共用资源
适用场景 绝大多数单元测试;每条用例需要独立干净测试数据 套件专属、初始化成本很高的重型资源(套件专用数据库、大配置文件) 程序全局资源(全局日志、全局命令行参数、全局初始化)
函数修饰 普通成员函数,无需 static 必须加 static 继承 Environment 类,重写虚函数 SetUp/TearDown
注册方式 TEST_F 宏自动管理,无需手动注册 TEST_F 宏自动管理,无需手动注册 需要手动调用 AddGlobalTestEnvironment 注册

一句话区分:

  • SetUp:一用例,一套环境,隔离优先,日常单元测试首选。
  • SetUpTestSuite:一套件,一次初始化,用来节省昂贵资源的重复创建。
  • Environment:整个程序,一次全局初始化,作用域覆盖全部测试套件。

八、GTest 面试总结要点

1、TEST 与 TEST_F 的区别

TEST 只定义独立测试函数,没有夹具,不存在自动 SetUp/TearDown。TEST_F 是测试夹具(Fixture),第一个参数必须是继承 testing::Test 的类;每一条 TEST_F 都会新建一个夹具实例,执行 SetUp,用例跑完调用 TearDown,最后销毁对象。普通成员变量天然实现用例间数据隔离。

2、SetUp / TearDown 和 SetUpTestSuite / TearDownTestSuite 的区别

  • **SetUp() / TearDown():**实例级钩子,每条测试用例前后都会执行;可以访问普通成员与静态成员。
  • SetUpTestSuite() / TearDownTestSuite(): 套件静态钩子,必须 static 修饰,当前套件所有用例执行前、结束后仅各执行一次,只能访问静态成员。 静态成员属于类,套件内部所有用例共享,存在状态互相污染风险。

3、testing::Environment 全局环境

  • 全局钩子,作用域是整个程序全部测试套件 ,程序启动所有套件前执行一次 SetUp,全部跑完执行一次 TearDown,需要手动调用AddGlobalTestEnvironment注册;适合全局资源初始化,和 TEST_F 夹具相互独立。

4、数据隔离与共享核心原则(高频考点)

单元测试核心:测试用例独立、可重复执行,不依赖执行顺序。

  • 夹具普通成员:绑定单个实例,天然隔离,推荐作为单元测试的测试数据。
  • static 静态成员:类级别共享,修改后会影响套件后续用例;尽量只存放只读重型资源,不要放可变业务测试数据。

注意: GTest 不保证套件内部用例执行顺序,一旦用例修改共享可变状态,会出现偶现不稳定测试(flaky test)。
5、断言 ASSERT_ 和 EXPECT_ 的区别

  • ASSERT_*: 断言失败直接终止当前这条测试用例,后续代码不再执行。
  • EXPECT_*: 断言失败不会中断当前用例,继续执行后面代码,收集多条失败信息。

结束语

本篇《C++ 工具库实战》GTest 单元测试框架全解,从框架安装、基础 TEST 宏与断言入手,一路深入事件机制、全局测试环境与局部测试夹具,结合实例剖析了 TEST_F 的数据隔离与共享底层逻辑,并对比了三类钩子的适用场景,最后整理了面试核心考点。

单元测试的核心思想,是保证每一段代码都拥有独立可复现的验证用例。SetUp/TearDown、套件静态钩子、全局 Environment 这些能力,本质上都是 GTest 为了帮我们管理测试资源而提供的工具。在实际项目中,我们应当优先利用实例夹具实现用例隔离,谨慎使用可变的静态共享状态,避免产生不稳定的测试。

相关推荐
余额瞒着我当琳9 小时前
C++异常,异常的概念,异常抛出与捕获,RAII 和智能指针的设计思路,C++标准库一些指针的使用
java·开发语言·c++·算法
東隅已逝,桑榆非晚9 小时前
map和set的使用
数据结构·c++·笔记·学习
骑着蜗牛撵大象3279 小时前
大模型 SDK 编译就崩?用 CMakeLists 把 httplib、JSON 与 OpenSSL 装配成可复用的 ChatServer
android·java·c++·http·json·cmake·大模型sdk
码小猿的CPP工坊10 小时前
C++跨平台开发之基于wxWidgets开发GUI程序简介-002
开发语言·c++
chenbingjie_c10 小时前
C++ 继承全解析(从三种继承方式到菱形继承与内存布局)
开发语言·c++
火22510 小时前
# C++ 微服务即时通讯系统(四):MySQL、Redis 与 Elasticsearch——数据分工与一致性
c++·mysql·微服务
xiaoye-duck20 小时前
《C++ 工具库实战》gflags 命令行参数解析库详解
c++·第三方库
狂奔的sherry21 小时前
C++ 宏生成访问函数的代价:访问权限、调试和类型检查
c++·宏·编译·预处理器·类型检查
划破黑暗的第一缕曙光1 天前
[C++]:5.STL:string(2)
java·c++·redis
小超人诞生日记1 天前
C++ 异常机制详解:从抛出捕获到异常安全
c++·算法