Google Test 官方 10 个 C++ 单元测试样例精解:从 TEST 宏到监听器 API(miniblink49 仓库实测)
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
本文以 miniblink49 仓库内携带的 Google Test 官方文档 V1_6_Samples.md 为主线,逐一对齐其列出的 10 个官方样例(samples 目录),从函数级断言、类方法测试、test fixture、参数化测试一路讲到监听器 API,并结合仓库中的真实源码说明每个宏与 API 的底层用法。读完本文,你可以掌握 Google Test 的完整入门路径,并直接在自己的 C++ 项目中复刻这套测试组织方式。
适用范围说明:以下所有路径与代码均来自当前仓库
v8_5_7/testing/gtest/目录,这是一份随 v8_5_7 一起携带、文档命名对应 1.6 系列的 Google Test 源码树。所有样例代码、断言语义、构建文件均以仓库实际内容为准。
1. 先从官方索引看全貌:10 个样例分别教什么
官方文档 V1_6_Samples.md 的核心内容是一句话结论:"If you're like us, you'd like to look at some Google Test sample code. The samples folder has a number of well-commented samples showing how to use a variety of Google Test features."——即 samples 目录是一组注释极其详尽的功能演示,下面这张对照表完整继承自文档,并补充了每个样例对应的被测代码:
| 样例文件 | 文档定义的主题 | 配套被测代码 |
|---|---|---|
| sample1_unittest.cc | 使用 Google Test 测试 C++ 函数的基本步骤 | sample1.cc / sample1.h |
| sample2_unittest.cc | 对一个含多个成员函数的类做更复杂的单测 | sample2.cc / sample2.h |
| sample3_unittest.cc | 使用 test fixture(测试夹具) | sample3-inl.h |
| sample4_unittest.cc | 另一个基础示例(强调断言参数只求值一次) | sample4.cc / sample4.h |
| sample5_unittest.cc | 通过派生子夹具在多个 test case 中复用同一夹具 | 复用 sample1、sample3 的被测代码 |
| sample6_unittest.cc | 类型参数化测试(typed / type-parameterized) | prime_tables.h |
| sample7_unittest.cc | 值参数化测试(value-parameterized)基础 | 同上 |
| sample8_unittest.cc | 值参数化测试中使用Combine()生成参数组合 | 同上 |
| sample9_unittest.cc | 用 listener API 改造控制台输出,用反射 API 检查测试结果 | — |
| sample10_unittest.cc | 用 listener API 实现一个简易内存泄漏检测器 | — |
文档原句逐一对应为:Sample 1 展示基本步骤,Sample 2 展示对多成员函数类的单测,Sample 3 使用 fixture,Sample 4 是另一个基础示例,Sample 5 教授如何通过派生子夹具在多个 test case 间复用夹具,Sample 6 演示类型参数化测试,Sample 7 教授值参数化测试基础,Sample 8 展示值参数化测试中的Combine(),Sample 9 展示 listener API 与反射 API,Sample 10 展示用 listener API 实现原始内存泄漏检查。下文按"基础断言 → 夹具 → 参数化 → 监听器"四条主线逐一精讲。
2. Sample 1~4:从零开始写第一个单测
2.1 Sample 1:函数级单测的"三步曲"
sample1_unittest.cc 的注释明确给出了 Google Test 写测试的三步:
- 引入头文件:既包含被测代码的头(
sample1.h),也不要忘记gtest/gtest.h——框架的所有断言与宏都由它声明。 - 用
TEST宏定义测试:TEST接受两个参数——test case 名称与测试名称,随后在一对大括号里写测试逻辑,用EXPECT_*系列宏断言成功或失败。 - 调用
RUN_ALL_TESTS():这一步通常不用自己写 main,而是链接 src/gtest_main.cc——该文件提供了一个现成的main(),内部调用RUN_ALL_TESTS()并返回 0(全部通过)或 1(存在失败)。
被测对象是 sample1.cc 中的两个函数:Factorial(int n)(返回 n!,负 n 按 1 处理)与IsPrime(int n)(素性判断,用 "试除到平方根" 的经典算法)。对应测试代码如下:
// Tests factorial of negative numbers. TEST(FactorialTest, Negative) { EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_GT(Factorial(-10), 0); } // Tests factorial of 0. TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); } TEST(FactorialTest, Positive) { EXPECT_EQ(1, Factorial(1)); EXPECT_EQ(2, Factorial(2)); EXPECT_EQ(6, Factorial(3)); EXPECT_EQ(40320, Factorial(8)); } TEST(IsPrimeTest, Negative) { EXPECT_FALSE(IsPrime(-1)); EXPECT_FALSE(IsPrime(-2)); EXPECT_FALSE(IsPrime(INT_MIN)); }这里蕴含三个关键设计决策,均可在样例注释的TechnicalDetails中找到原始解释:
- 测试分组:逻辑相关的测试放进同一个 test case(如
FactorialTest、IsPrimeTest)。test case 名与测试名都必须是合法 C++ 标识符,且官方建议不要使用下划线。 - 执行语义:Google Test 保证每个测试恰好执行一次,但不保证执行顺序,因此测试必须互不依赖、结果与顺序无关。
EXPECT_EQ优于裸EXPECT_TRUE:EXPECT_EQ(expected, actual)等价于EXPECT_TRUE((expected) == (actual)),但失败时会把期望值与实际值同时打印出来,极大方便调试;EXPECT_TRUE更通用,可接收任意布尔表达式。
2.2 Sample 2:类的多成员函数测试
sample2_unittest.cc 针对 sample2.h 中一个自实现的极简字符串类MyString(含默认构造、C 字符串构造、拷贝构造、Set、Length、c_string等成员)进行测试。官方建议"每个成员方法对应一个测试",本例便是这一组织方式的示范:
TEST(MyString, DefaultConstructor) { const MyString s; // EXPECT_STREQ(NULL, s.c_string()); EXPECT_EQ(0u, s.Length()); } TEST(MyString, Set) { MyString s; s.Set(kHelloString); EXPECT_EQ(0, strcmp(s.c_string(), kHelloString)); // Set should work when the input pointer is the same as the one // already in the MyString object. s.Set(s.c_string()); // Can we set the MyString to NULL? s.Set(NULL); EXPECT_STREQ(NULL, s.c_string()); }本样例最有价值的两个技术点:
EXPECT_STREQ用于 C 风格字符串:比较的是内容而非指针,失败时打印两个字符串本身。NULL的类型陷阱:注释详细记录了一个 gcc 3.4 时代的经典警告——直接写EXPECT_EQ(NULL, ...)时,由于NULL被宏定义为整数0,编译器会按int选择格式化函数,而 gcc 认为NULL应作指针用而报警告。根源是 C++ 没有区分"整数 0"与"空指针常量"。因此对指针判空应显式写成static_cast<const char*>(NULL)或用EXPECT_STREQ。
2.3 Sample 3:test fixture——共享初始化与清理逻辑
sample3_unittest.cc 引入 Google Test 的核心抽象test fixture(测试夹具)。被测代码是 sample3-inl.h 中的Queue<int>模板队列类。
使用方法:从testing::Test派生子类,把共享对象和辅助函数放进protected区,然后:
class QueueTest : public testing::Test { protected: // SetUp() 在每条测试运行前被调用,用于初始化共享变量 virtual void SetUp() { q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } // TearDown() 在每条测试运行后被调用,若无清理工作可省略 // 供测试复用的辅助函数 void MapTester(const Queue<int>* q) { /* ... */ } // 测试要使用的共享变量 Queue<int> q0_; Queue<int> q1_; Queue<int> q2_; }; // 使用夹具时,用 TEST_F 而非 TEST TEST_F(QueueTest, DefaultConstructor) { EXPECT_EQ(0u, q0_.Size()); } TEST_F(QueueTest, Dequeue) { int* n = q1_.Dequeue(); ASSERT_TRUE(n != NULL); // 致命断言:失败立即中止当前测试 EXPECT_EQ(1, *n); delete n; }三个必须记住的语义(均来自样例TechnicalDetails):
- 代码共享,数据不共享:每条测试都会获得一份全新的夹具实例,
SetUp()/TearDown()在每条测试前后执行。一个测试修改的数据绝不应被另一个测试观察到——这正是"测试必须独立、可重复"的保障。 - 断言宏需要"当前测试"上下文:
EXPECT_TRUE、FAIL等宏实际调用的是Test类的成员函数,因此不能在全局函数中使用,这正解释了为什么要用夹具承载辅助子过程。 ASSERT_*与EXPECT_*的分工:ASSERT_TRUE/ASSERT_EQ等致命断言一旦失败,立即中止当前测试(防止在空指针上继续操作);EXPECT_*非致命失败则让测试继续跑完,用于收集尽可能多的失败信息。上例先ASSERT_TRUE(n != NULL)再EXPECT_EQ(1, *n)就是标准的安全写法。
2.4 Sample 4:断言参数只求值一次
sample4_unittest.cc 被测对象是 sample4.h 的计数器类Counter。核心测试只有一段,重点在注释揭示的语义:
TEST(Counter, Increment) { Counter c; // EXPECT_EQ() evaluates its arguments exactly once, so they // can have side effects. EXPECT_EQ(0, c.Increment()); EXPECT_EQ(1, c.Increment()); EXPECT_EQ(2, c.Increment()); }EXPECT_EQ()会对参数精确求值一次,因此参数可以安全地携带副作用(如Increment()自增并返回旧值),连续断言能可靠验证 0→1→2 的计数轨迹。
3. Sample 5:夹具继承——一处实现,多处复用
sample5_unittest.cc 解决一个现实问题:一个夹具只能被一个 test case 使用(因为TEST_F的第一个参数必须与夹具类同名)。当多个 test case 需要相同的前置/后置逻辑时,官方做法是"super fixture(超类夹具)+ 子夹具派生"。
样例中的业务场景是:希望保证每条测试都在约 5 秒内结束,超时即失败。实现方式是把计时逻辑写进超类夹具QuickTest:
class QuickTest : public testing::Test { protected: // SetUp() 在测试开始前立即执行,这里记录起始时间 virtual void SetUp() { start_time_ = time(NULL); } // TearDown() 在测试结束后立即执行,这里检查耗时 virtual void TearDown() { const time_t end_time = time(NULL); // 注意:SetUp() 和 TearDown() 里同样可以使用断言! EXPECT_TRUE(end_time - start_time_ <= 5) << "The test took too long."; } time_t start_time_; };然后派生子夹具,让不同 test case 各自复用:
// 子夹具 1:无额外逻辑,空实现即可 class IntegerFunctionTest : public QuickTest { }; // 子夹具 2:既有 QuickTest 的超时检查,又有自己的共享对象 class QueueTest : public QuickTest { protected: virtual void SetUp() { QuickTest::SetUp(); // 先初始化超类夹具 q1_.Enqueue(1); // 再做本夹具的额外初始化 q2_.Enqueue(2); q2_.Enqueue(3); } Queue<int> q0_, q1_, q2_; };随后TEST_F(IntegerFunctionTest, Factorial)、TEST_F(IntegerFunctionTest, IsPrime)、TEST_F(QueueTest, Dequeue)各自继承超时约束。注释还指出:夹具继承层级可以继续加深(例如再从一个派生夹具派生),Google Test 不限制深度,但实践中不宜过深以免难以理解。这个模式非常适合 GUI 库等场景——例如统一检查每个测试是否泄漏字体、画刷等系统资源。
4. Sample 6~8:参数化测试的三件套
参数化测试要解决的共性问题:对同一接口的多个实现(或同一逻辑的多组输入)重复执行同一组断言。三个样例围绕 prime_tables.h 中PrimeTable接口的两个实现展开:OnTheFlyPrimeTable(运行时实时判素数)与PreCalculatedPrimeTable(预计算素数表,构造参数为表容量)。
4.1 Sample 6:类型参数化测试(typed tests 与 type-parameterized tests)
sample6_unittest.cc 展示了两种做法,适用场景不同:
方式一:typed tests(已知全部类型)。当你编写测试时就已经确定要测哪些类型,用TYPED_TEST_CASE声明类型列表、TYPED_TEST定义测试:
typedef Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable> Implementations; TYPED_TEST_CASE(PrimeTableTest, Implementations); TYPED_TEST(PrimeTableTest, ReturnsTrueForPrimes) { EXPECT_TRUE(this->table_->IsPrime(2)); EXPECT_TRUE(this->table_->IsPrime(3)); // ... }Google Test 会自动把每个TYPED_TEST对类型列表中的每个类型各跑一遍。注意:由于进入模板世界,访问夹具成员必须显式写this->(样例注释强调这是 C++ 模板依赖查找规则使然)。
方式二:type-parameterized tests(未来类型未知)。如果你是接口作者,希望别人后续实现也能复用你的测试,就用TYPED_TEST_CASE_P+TYPED_TEST_P定义"测试模式",然后用REGISTER_TYPED_TEST_CASE_P登记、用INSTANTIATE_TYPED_TEST_CASE_P实例化:
TYPED_TEST_CASE_P(PrimeTableTest2); TYPED_TEST_P(PrimeTableTest2, CanGetNextPrime) { /* ... */ } // 登记模式中定义的所有测试名 REGISTER_TYPED_TEST_CASE_P(PrimeTableTest2, ReturnsFalseForNonPrimes, ReturnsTrueForPrimes, CanGetNextPrime); // 用实例名 + 类型列表把抽象模式变成真实测试 typedef Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable> PrimeTableImplementations; INSTANTIATE_TYPED_TEST_CASE_P(OnTheFlyAndPreCalculated, PrimeTableTest2, PrimeTableImplementations);实例名会成为 test case 名的一部分,可用于测试过滤;同一模式可以在同一程序里多次实例化(用不同实例名区分)。两者均受特性宏GTEST_HAS_TYPED_TEST/GTEST_HAS_TYPED_TEST_P保护。
4.2 Sample 7:值参数化测试(value-parameterized tests)
sample7_unittest.cc 的适用场景是:同一套测试要作用于一组"值"参数。这里参数被设计为指向工厂函数的指针(CreatePrimeTableFunc*),每个参数值对应一种PrimeTable实现:
class PrimeTableTest : public TestWithParam<CreatePrimeTableFunc*> { public: virtual void SetUp() { table_ = (*GetParam())(); } // 用参数创建被测对象 virtual void TearDown() { delete table_; table_ = NULL; } protected: PrimeTable* table_; }; TEST_P(PrimeTableTest, ReturnsFalseForNonPrimes) { EXPECT_FALSE(table_->IsPrime(-5)); EXPECT_FALSE(table_->IsPrime(0)); // ... } // 把测试绑定到一组参数值上 INSTANTIATE_TEST_CASE_P( OnTheFlyAndPreCalculated, PrimeTableTest, Values(&CreateOnTheFlyPrimeTable, &CreatePreCalculatedPrimeTable<1000>));要点:
- 夹具继承
TestWithParam<T>,测试体内用GetParam()取当前参数;参数化测试用TEST_P定义,用INSTANTIATE_TEST_CASE_P实例化。 - 通用规则(样例注释原文):为防止一个测试影响后续测试,应每条测试独立创建、销毁被测对象,而不是复用,本例即把创建放在
SetUp()、销毁放在TearDown()。 - 测试通过
PrimeTable基类接口而非具体实现类来断言,更贴近真实调用场景,也能避开子类方法遮蔽基类同名重载这类陷阱。
4.3 Sample 8:Combine()生成参数全组合
sample8_unittest.cc 针对一个更复杂的被测对象HybridPrimeTable(组合了预计算表的快与实时计算的灵活,并支持低内存模式下禁用预计算表)。它有两个构造参数:bool force_on_the_fly与int max_precalculated,需要覆盖两者的全组合:
using ::testing::TestWithParam; using ::testing::Bool; using ::testing::Values; using ::testing::Combine; class PrimeTableTest : public TestWithParam< ::testing::tuple<bool, int> > { protected: virtual void SetUp() { bool force_on_the_fly = ::testing::get<0>(GetParam()); int max_precalculated = ::testing::get<1>(GetParam()); table_ = new HybridPrimeTable(force_on_the_fly, max_precalculated); } // ... }; INSTANTIATE_TEST_CASE_P( OnTheFlyAndPreCalculated, PrimeTableTest, Combine(Bool(), Values(1, 10, 100)));Combine()会对每个参数发生器(Bool()产生 true/false,Values(1, 10, 100)产生三个整数)做笛卡尔积,本例共生成 2×3=6 组参数;测试体内用::testing::get<0>(GetParam())/get<1>取出各分量(注释提到待 C++ 风格指南允许后可用std::tr1::tie一次性解包)。该功能受GTEST_HAS_COMBINE宏保护。
5. Sample 9~10:监听器 API 的两大实战
5.1 Sample 9:自定义控制台输出 + 反射 API 检查结果
sample9_unittest.cc 展示两条高级 API:
listener API:继承::testing::EmptyTestEventListener,按需重写事件回调(所有回调都在测试进程的各个生命周期节点被触发):
class TersePrinter : public EmptyTestEventListener { private: // 整个测试程序开始前 virtual void OnTestProgramStart(const UnitTest&) {} // 整个测试程序结束后 virtual void OnTestProgramEnd(const UnitTest& unit_test) { fprintf(stdout, "TEST %s\n", unit_test.Passed() ? "PASSED" : "FAILED"); fflush(stdout); } // 单条测试开始 virtual void OnTestStart(const TestInfo& test_info) { fprintf(stdout, "*** Test %s.%s starting.\n", test_info.test_case_name(), test_info.name()); fflush(stdout); } // 一条断言失败或 SUCCEED() 被调用时 virtual void OnTestPartResult(const TestPartResult& test_part_result) { fprintf(stdout, "%s in %s:%d\n%s\n", test_part_result.failed() ? "*** Failure" : "Success", test_part_result.file_name(), test_part_result.line_number(), test_part_result.summary()); fflush(stdout); } // 单条测试结束 virtual void OnTestEnd(const TestInfo& test_info) { /* ... */ } };通过TestEventListeners把自定义监听器注册进UnitTest::GetInstance(),即可替换默认输出为极简风格。同时该样例还演示了UnitTest 反射 API:通过UnitTest、TestCase、TestInfo等类型枚举所有 test case 与测试、检查其运行结果。注意main()中应使用InitGoogleTest初始化框架。
5.2 Sample 10:用监听器实现内存泄漏检测
sample10_unittest.cc 是 listener API 最经典的实战:原始泄漏检查器。思路分三层:
- 追踪被测对象:给类
Water重载operator new/operator delete,用静态计数器allocated_记录存活实例数:
class Water { public: void* operator new(size_t allocation_size) { allocated_++; return malloc(allocation_size); } void operator delete(void* block, size_t) { allocated_--; free(block); } static int allocated() { return allocated_; } private: static int allocated_; };- 监听器比对前后差值:
LeakChecker在OnTestStart记录初始存活数,在OnTestEnd计算差值,若差值为正则用断言报告泄漏:
class LeakChecker : public EmptyTestEventListener { private: virtual void OnTestStart(const TestInfo&) { initially_allocated_ = Water::allocated(); } virtual void OnTestEnd(const TestInfo&) { int difference = Water::allocated() - initially_allocated_; EXPECT_LE(difference, 0) << "Leaked " << difference << " unit(s) of Water!"; } int initially_allocated_; };- 两条对比例证:
TEST(ListenersTest, DoesNotLeak)(new 后 delete,通过)与TEST(ListenersTest, LeaksWater)(new 后不 delete,携带--check_for_leaks命令行标志运行时失败)。样例注释特别提醒:除OnTestPartResult外的任何事件回调里都可以安全地使用 Google Test 断言。
6. 如何构建与运行这些样例
这些样例不是孤立文件,而是完整 gtest 源码树的一部分,仓库为其提供了多套构建入口:
- 被测代码与测试文件全部位于 v8_5_7/testing/gtest/samples,框架实现位于 v8_5_7/testing/gtest/src(如 gtest-all.cc、gtest-death-test.cc),公开头文件位于 include/gtest。
- 顶层 CMakeLists.txt 与 Makefile.am(autotools)都定义了样例目标;另有
msvc/与xcode/工程目录,可分别在 Windows(Visual Studio)与 macOS(Xcode)下打开构建;scripts/目录还提供了生成单文件版 gtest 的融合脚本。具体构建步骤以仓库根目录的 README.md 及对应构建文件为准。 - 若想脱离现成工程手动编译,只需把被测源文件(如
sample1.cc)与对应*_unittest.cc一起编译,并链接 gtest 库与 src/gtest_main.cc 提供的main();该main()内部调用RUN_ALL_TESTS(),程序返回 0 表示全部通过,返回 1 表示存在失败。
提醒:样例普遍使用
#if GTEST_HAS_PARAM_TEST、#if GTEST_HAS_TYPED_TEST、#if GTEST_HAS_COMBINE等特性宏包裹参数化测试代码,编译环境是否启用这些特性以 include/gtest 中的实际配置为准。
7. 小结:一条清晰的 Google Test 学习路线
把官方索引的 10 个样例串起来,正好是一条由浅入深的能力曲线:
- Sample 1~2:
TEST宏 +EXPECT_*/ASSERT_*断言族,覆盖函数与类两种被测对象,理解"测试用例分组"与"断言只求值一次"。 - Sample 3~5:test fixture(
SetUp/TearDown/TEST_F)解决初始化与清理的复用,夹具继承(super fixture)把公共约束(如超时、资源泄漏)批量施加到多个 test case。 - Sample 6~8:typed tests、type-parameterized tests、value-parameterized tests 与
Combine(),覆盖"同一套断言 × 多实现/多参数"的全部形态。 - Sample 9~10:listener API 与反射 API,把测试框架从"跑断言"升级为"可编程的测试基础设施"——自定义输出、实现泄漏检查器只是两个起点。
无论你是刚接触 Google Test 的新手,还是想复用这套模式为 miniblink49 这样的大型 C++ 项目编写结构化测试,都可以直接以 v8_5_7/testing/gtest/samples 目录为活教材:每个文件注释详尽、可直接编译运行,是比任何教程都更贴近真实代码的官方样例。
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考