GTest 快速入门
一、环境准备
- 编译器:支持 C++17 及以上标准。
- 构建工具:CMake(3.14+) + Make/Ninja 等。
支持平台:Linux、macOS、Windows。
二、项目文件布局
这是一个极简的 GTest 项目目录结构:
my_project/ # 项目根目录 ├── CMakeLists.txt # CMake 构建配置文件 └── hello_test.cc # 测试源码文件构建临时目录(由命令生成,不属于源码):
build/
三、CMakeLists.txt 核心内容拆解
这份文件分为三大块:基础配置、拉取 GTest 依赖、构建测试目标。
# 1. 基础配置
cmake_minimum_required(VERSION 3.14)
project(my_project)
set(CMAKE_CXX_STANDARD 17) # 必须 C++17
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 2. 拉取 GoogleTest 依赖(使用 FetchContent)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/[提交哈希].zip # 建议常更新哈希值
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Windows 特定设置,防止链接冲突
FetchContent_MakeAvailable(googletest)
# 2.5 这里也可以使用 vcpkg 的方式引入。
# 3. 构建与测试目标
enable_testing() # 启用 CMake 测试
add_executable(hello_test hello_test.cc) # 生成测试可执行文件
target_link_libraries(hello_test GTest::gtest_main) # 链接 GTest 主入口库
include(GoogleTest) # 引入 GTest CMake 模块
gtest_discover_tests(hello_test) # 自动发现并注册测试用例四、测试源码内容
#include <gtest/gtest.h> // 必须包含 GTest 主头文件
// 定义一个测试:套件名 HelloTest,用例名 BasicAssertions
TEST(HelloTest, BasicAssertions) {
// 断言字符串不相等
EXPECT_STRNE("hello", "world");
// 断言数值相等
EXPECT_EQ(7 * 6, 42);
}五、编译与运行命令速查
在项目根目录(my_project)下执行:
| 步骤 | 命令 | 说明 |
|---|---|---|
| 配置 | cmake -S . -B build | 生成构建文件到 build/ 目录 |
| 编译 | cmake --build build | cmake --build build |
| 测试 | cd build && ctest | 进入构建目录,运行所有测试用例 |
- 依赖管理方式:官方推荐使用
FetchContent模块直接从 GitHub 拉取源码,无需手动下载安装 GTest。 - 链接库选择:链接
GTest::gtest_main会自动提供main()函数入口,无需自己写main;若链接GTest::gtest,则需手动定义RUN_ALL_TESTS()。 - 测试发现机制:
gtest_discover_tests会自动扫描二进制文件中的测试宏,无需手动列出每个测试名,极大方便了维护。
GTest 入门补充
1. 断言表
EXPECT_*失败后继续执行ASSERT_*失败立刻终止当前函数。
- 值相等:
EXPECT_EQ(val1, val2)/ASSERT_EQ - 值不等:
EXPECT_NE(val1, val2)/ASSERT_NE - 条件为真:
EXPECT_TRUE(condition)/ASSERT_TRUE - 条件为假:
EXPECT_FALSE(condition)/ASSERT_FALSE - 字符串相等:
EXPECT_STREQ(str1, str2)/ASSERT_STREQ - 字符串不等:
EXPECT_STRNE(str1, str2)/ASSERT_STRNE
2. 添加自定义失败消息
非常简单的示例,一看就懂,就是在 断言后面添加 << 和要输出的内容。
EXPECT_EQ(a, b) << "额外调试信息: a=" << a << ", b=" << b;- 帮助新人快速定位失败位置
3. 测试夹具 (Test Fixture) 基础
如果多个测试需要共享相同的初始数据或辅助方法,使用 TEST_F 宏。
#include <gtest/gtest.h>
// 1. 定义一个夹具类,继承 testing::Test
class MyTest : public ::testing::Test {
protected:
// 2. 初始化工作放在 SetUp 里(每个测试前自动调用)
void SetUp() override {
value = 42;
data = new int[100]; // 假设我们分配了动态内存
}
// 3. 清理工作放在 TearDown 里(每个测试后自动调用)
void TearDown() override {
delete[] data; // 释放动态内存,防止泄漏
data = nullptr;
}
// 测试夹具的成员变量,每个测试都可直接使用
int value;
int* data = nullptr;
};
// 4. 使用 TEST_F 代替 TEST,第一个参数必须是夹具类名
TEST_F(MyTest, TestA) {
// 可以直接访问 value 和 data
EXPECT_EQ(value, 42);
EXPECT_NE(data, nullptr);
// 给 data 赋值
data[0] = 10;
}
TEST_F(MyTest, TestB) {
// TestB 拥有全新的夹具实例
// value 依然是 42,data 依然是新分配的数组
EXPECT_EQ(value, 42);
// data[0] 还是初始值 0,不会受 TestA 影响
EXPECT_EQ(data[0], 0);
}生命周期(自动调用机制)
对于每个 TEST_F,框架严格执行以下 5 步,无需手动干预:
- 构造:创建一个全新的夹具对象。
SetUp():自动调用,执行初始化。- 运行测试体:执行
TEST_F中你写的代码。 TearDown():自动调用,执行清理。- 销毁:析构夹具对象。
必须遵守的关键规则
1. 命名规范
- 方法名 必须是
SetUp(U大写)和TearDown(D大写)。 务必加上
override关键字,写错会立刻编译报错。void SetUp() override { ... } // √ 正确 void Setup() override { ... } // × 编译不通过
- 测试隔离性
- 每个测试会新建一个夹具实例,测试之间不共享状态。
- 如示例中
TestA对data[0]的修改,绝对不会影响到TestB。
3. SetUp/TearDown 于 构造/析构函数
| 构造函数 / 析构函数 | SetUp() / TearDown() | |
|---|---|---|
| 虚函数支持 | 不支持,无法调用子类的重载版本 | 完全支持 |
| 异常处理 | 析构函数抛异常极危险,易致崩溃 | 可安全地记录错误,不抛异常 |
| 错误信息 | 失败信息不明确 | 可用 GTest 宏输出明确的错误日志 |
| 推荐度 | 仅用于极简单的值初始化 | 首选方案 |
简单记:任务拆开,构造和析构只管资源本身(可选),所有测试的“准备动作”和“清扫动作”都交给 SetUp 和 TearDown,这样更安全、更规范。
4. 命名规范提醒
TEST(SuiteName, TestName) 中的两个名称都 不应包含下划线(_),否则可能与框架内部标识符冲突。
例如:TEST(Foo, Bar_test) 错位 → TEST(Foo, BarTest) 正确。
5. 运行特定测试(过滤)
提到在命令行中如何只跑部分测试,会非常实用:
./hello_test --gtest_filter=HelloTest.* # 运行某个套件全部
./hello_test --gtest_filter=*Basic* # 名称含 Basic 的测试也可以说明 ctest 环境下如何传递参数(如 ctest -R <regex> 按测试名过滤)。
6. 测试输出解读
简要解释运行后的输出:通过的点表示成功,失败会打印文件名、行号、期望值和实际值。让新手知道如何读报错。
GTest 高级特性(按使用频率排序)
第一梯队:日常调试与快速筛选
测试筛选(--gtest_filter)
# 运行所有测试 ./my_test # 只运行 FooTest 套件下的所有测试 ./my_test --gtest_filter=FooTest.* # 运行名称包含 "Null" 或 "Constructor" 的测试 ./my_test --gtest_filter=*Null*:*Constructor* # 运行 FooTest 下除了 Bar 之外的所有测试 ./my_test --gtest_filter=FooTest.*-FooTest.Bar # 组合:运行 A 和 B 套件,但排除特定的几个 ./my_test --gtest_filter=FooTest.*:BarTest.*-FooTest.Bar:BarTest.Foo # 仅列出所有测试名称(不执行) ./my_test --gtest_list_tests临时禁用测试(DISABLED_ 前缀)
// 单个测试禁用 TEST(FooTest, DISABLED_DoesAbc) { // 不会被执行 } // 整个套件禁用 class DISABLED_BarTest : public testing::Test {}; TEST_F(DISABLED_BarTest, DoesXyz) { // 不会被执行 } // 强制运行禁用的测试(命令行) ./my_test --gtest_also_run_disabled_tests动态跳过测试(GTEST_SKIP)
TEST(SkipTest, NeedsDatabase) { if (!ConnectToDatabase()) { GTEST_SKIP() << "Database not available, skipping test."; } // 以下代码仅在数据库连接成功时执行 EXPECT_TRUE(QueryData()); } // 在 SetUp 中跳过整个 Fixture class DBTest : public testing::Test { void SetUp() override { if (!InitDB()) { GTEST_SKIP() << "Skipping all DB tests due to init failure."; } } }; // 该 Fixture 下所有测试将被跳过追踪上下文(SCOPED_TRACE)
// 用于循环或多层调用,帮助定位失败发生在哪一次调用 void TestValue(int n) { SCOPED_TRACE(n); // 将 n 加入失败信息 EXPECT_GT(n, 0); } TEST(TraceTest, Loop) { for (int i = -2; i <= 2; i++) { SCOPED_TRACE(testing::Message() << "Iteration: " << i); TestValue(i); // 失败时会显示具体是哪个 i 导致的 } } // 输出会显示: path/to/file.cc:line: Iteration: -1
第二梯队:测试结构组织与数据驱动
值参数化测试(TEST_P + INSTANTIATE_TEST_SUITE_P)
// 1. 定义 Fixture class MathTest : public testing::TestWithParam<int> { void SetUp() override { value_ = GetParam(); } protected: int value_; }; // 2. 写测试逻辑 TEST_P(MathTest, IsPositive) { EXPECT_GT(value_, 0); } // 3. 实例化(传入参数列表) INSTANTIATE_TEST_SUITE_P( PositiveNumbers, MathTest, testing::Values(1, 5, 10, 20) // 每个值运行一次 ); // 多参数组合(Combine) class CombineTest : public testing::TestWithParam<std::tuple<int, bool>> {}; TEST_P(CombineTest, Check) { int i = std::get<0>(GetParam()); bool b = std::get<1>(GetParam()); // ... } INSTANTIATE_TEST_SUITE_P( MyCombos, CombineTest, testing::Combine(testing::Values(1, 2), testing::Values(true, false)) ); // 生成 4 个测试: (1,true), (1,false), (2,true), (2,false)
INSTANTIATE_TEST_SUITE_P():
- 第一个参数:测试实例前缀 -> 用于生成最终的测试名称。
- 第二个参数:类名(必须与 TEST_P 中定义的类名称完全一致) -> 指定你要实例化哪个参数化测试类。
第三个参数:参数生成器 -> 提供具体的参数值列表。
- 常用内置生成器:
testing::Values(v1, v2, ...):直接枚举固定值。testing::ValuesIn(container):从 STL 容器(如vector、list)或数组迭代器中取值。testing::Range(start, end, step):生成等差数列。testing::Bool():生成false和true。testing::Combine(g1, g2, ...):生成多个生成器的笛卡尔积(需要参数是std::tuple类型)。
- 第四个参数:
自定义命名函数—— 可选
测试套件级 SetUp/TearDown(共享昂贵资源)
class SharedResourceTest : public testing::Test { protected: static int* shared_data_; // 静态共享资源 static void SetUpTestSuite() { shared_data_ = new int(42); // 整个套件只初始化一次 } static void TearDownTestSuite() { delete shared_data_; shared_data_ = nullptr; } }; int* SharedResourceTest::shared_data_ = nullptr; TEST_F(SharedResourceTest, ReadData) { EXPECT_EQ(*shared_data_, 42); // 所有测试共享同一份数据 }自定义值打印(提升失败信息可读性)
// 方式一:定义 AbslStringify(推荐) namespace my_namespace { class Point { public: Point(int x, int y) : x_(x), y_(y) {} template <typename Sink> friend void AbslStringify(Sink& sink, const Point& p) { absl::Format(&sink, "(%d, %d)", p.x_, p.y_); } private: int x_, y_; }; } // namespace my_namespace TEST(PrintTest, Custom) { Point p(3, 4); EXPECT_EQ(p, Point(1, 2)); // 失败时输出 "Actual: (3,4) Expected: (1,2)" } // 方式二:定义 PrintTo(如果不想依赖 Abseil) void PrintTo(const Point& p, std::ostream* os) { *os << "(" << p.x_ << "," << p.y_ << ")"; }
第三梯队:特殊场景验证
死亡测试(崩溃/退出验证)
void Foo(int* p) { if (!p) { fprintf(stderr, "Error: Null pointer!\n"); exit(1); } } TEST(DeathTest, FooWithNull) { // 断言进程死亡,且 stderr 匹配正则 ASSERT_DEATH(Foo(nullptr), "Null pointer"); } TEST(DeathTest, NormalExit) { // 检查正常退出码 EXPECT_EXIT(exit(0), testing::ExitedWithCode(0), ""); } TEST(DeathTest, SignalKill) { // 检查收到信号 EXPECT_EXIT(raise(SIGKILL), testing::KilledBySignal(SIGKILL), ""); } // 注意:测试套件名建议以 DeathTest 结尾浮点数比较
TEST(FloatTest, Compare) { double a = 0.1 + 0.2; double b = 0.3; // 绝对误差比较 EXPECT_NEAR(a, b, 1e-9); // 浮点数关系(小于等于近似) using ::testing::FloatLE; using ::testing::DoubleLE; EXPECT_PRED_FORMAT2(FloatLE, 1.0f, 1.01f); // 1.0 <= ~1.01 EXPECT_PRED_FORMAT2(DoubleLE, 0.3, 0.300001); }类型测试(用于模板验证)
// 针对多种类型执行相同测试 template <typename T> class MyTypeTest : public testing::Test { protected: T value_ = T(0); }; // 定义要测试的类型列表 using MyTypes = testing::Types<int, float, double>; TYPED_TEST_SUITE(MyTypeTest, MyTypes); // 每个类型都会执行此测试 TYPED_TEST(MyTypeTest, IsPositive) { TypeParam n = this->value_; EXPECT_GE(n, 0); }全局环境(程序级 Setup)
class MyGlobalEnv : public testing::Environment { public: void SetUp() override { // 在所有测试开始前执行(如启动 Mock 服务) printf("Global Setup\n"); } void TearDown() override { // 在所有测试结束后执行 printf("Global Teardown\n"); } }; // 在 main() 中注册 int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); testing::AddGlobalTestEnvironment(new MyGlobalEnv); return RUN_ALL_TESTS(); }
第四梯队:CI/CD 与批量执行策略
生成 XML / JSON 报告
# 生成 XML ./my_test --gtest_output=xml:report.xml # 或仅指定目录(自动命名) ./my_test --gtest_output=xml:test_results/ # 生成 JSON ./my_test --gtest_output=json:report.json # 环境变量方式 export GTEST_OUTPUT=xml:report.xml ./my_test重复执行与随机顺序
# 重复执行 100 次(复现偶发性失败) ./my_test --gtest_repeat=100 # 无限重复(按 Ctrl+C 停止) ./my_test --gtest_repeat=-1 # 重复直到第一次失败(配合调试器) ./my_test --gtest_repeat=1000 --gtest_break_on_failure # 随机顺序执行 ./my_test --gtest_shuffle # 指定随机种子(复现上次顺序) ./my_test --gtest_shuffle --gtest_random_seed=12345失败快速退出与分片
# 遇到第一个失败立即退出 ./my_test --gtest_fail_fast # 分片执行(3 台机器并行) # 机器 0 export GTEST_TOTAL_SHARDS=3 export GTEST_SHARD_INDEX=0 ./my_test # 机器 1 export GTEST_SHARD_INDEX=1 ./my_test # 机器 2 export GTEST_SHARD_INDEX=2 ./my_test
第五梯队:高级扩展与底层控制
捕获子例程中的致命失败
void Subroutine() { ASSERT_EQ(1, 2); // 这里会失败 } TEST(FooTest, Bar) { // 方法一:断言子例程不产生致命失败 ASSERT_NO_FATAL_FAILURE(Subroutine()); // 方法二:手动检查并提前返回 Subroutine(); if (testing::Test::HasFatalFailure()) { return; // 避免执行后续危险的代码 } // 安全代码... }断言捕获(测试你的测试工具)
#include "gtest/gtest-spi.h" TEST(UtilTest, CheckFailure) { // 验证某段代码确实会产生非致命失败 EXPECT_NONFATAL_FAILURE( EXPECT_EQ(1, 2), // 这里会失败 "Value of: 2" // 期望的错误信息片段 ); // 验证会产生致命失败 EXPECT_FATAL_FAILURE( ASSERT_TRUE(false), "Value of: false" ); }测试私有代码(FRIEND_TEST)
// foo.h class Foo { private: int Secret(int x) { return x * 2; } FRIEND_TEST(FooTest, SecretReturnsDouble); // 授权测试访问 }; // foo_test.cc TEST(FooTest, SecretReturnsDouble) { Foo f; EXPECT_EQ(f.Secret(5), 10); // 直接访问私有方法 }动态注册测试(RegisterTest)
// 适用于运行时从文件/配置读取测试参数 class DynamicFixture : public testing::Test { public: explicit DynamicFixture(int data) : data_(data) {} void TestBody() override { EXPECT_GT(data_, 0); } private: int data_; }; // 在 main() 中注册 int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); std::vector<int> values = LoadFromConfig(); // {1, 2, 3} for (int v : values) { testing::RegisterTest( "DynamicSuite", // 套件名 ("Test" + std::to_string(v)).c_str(), // 测试名 nullptr, // 类型参数(一般不用) std::to_string(v).c_str(), // 值参数 __FILE__, __LINE__, [v]() -> testing::Test* { return new DynamicFixture(v); } ); } return RUN_ALL_TESTS(); }Sanitizer 集成(将 UB 转为测试失败)
// 放在主二进制编译的任意 .cpp 中 extern "C" { void __ubsan_on_report() { FAIL() << "Undefined Behavior Sanitizer error detected!"; } void __asan_on_error() { FAIL() << "Address Sanitizer error detected!"; } void __tsan_on_report() { FAIL() << "Thread Sanitizer error detected!"; } }自定义事件监听器(替换输出)
class CustomListener : public testing::EmptyTestEventListener { void OnTestStart(const testing::TestInfo& info) override { printf("▶ Running: %s.%s\n", info.test_suite_name(), info.name()); } void OnTestEnd(const testing::TestInfo& info) override { printf("√ %s\n", info.result()->Passed() ? "PASS" : "FAIL"); } }; int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); auto& listeners = testing::UnitTest::GetInstance()->listeners(); // 移除默认输出 delete listeners.Release(listeners.default_result_printer()); // 添加自定义 listeners.Append(new CustomListener); return RUN_ALL_TESTS(); }
