miniblink49 中的 Google Test v1.7:C++ 单元测试 Primer 完全指南(断言、Fixture 与测试执行机制)
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
miniblink49 仓库在v8_6_7/testing/下完整收录了 Google Test(Google C++ Testing Framework,简称 Google Test / GTest)v1.7 的源码与文档。本文以官方 Primer(入门指南)V1_7_Primer.md 为骨架,完整继承其中的断言宏体系、Fixture 用法、main()编写规范与已知限制,并结合仓库内 gtest v1.7 的真实头文件与实现(gtest.h、gtest-internal.h、gtest-port.h等)逐一印证其底层机制。读完后,你可以直接在 miniblink49 这类 C++ 项目中搭建一个可编译、可运行、可注册的测试工程,并理解TEST()/TEST_F()背后的静态注册原理。
为什么需要 Google Test:官方对"好测试"的六条标准
Primer 开篇给出了框架设计哲学,这六条标准也是理解后文所有 API 的基础:
- 独立且可重复(independent and repeatable):框架让每个测试运行在不同的对象上以相互隔离;某个测试失败时可以单独重跑,便于调试。
- 组织有序(well organized):相关测试被归入 test case,共享数据与子例程,测试结构反映被测代码结构,方便跨项目迁移人员维护。
- 可移植且可复用(portable and reusable):在 Linux、Windows、Mac 上,配合 gcc、MSVC 等不同编译器、有无异常支持的配置均可工作。文档同时注明:当前发行版的构建脚本只提供 Linux 的 autotools 脚本(已废弃),其他平台脚本当时仍在开发中——而仓库内的 CMakeLists.txt 正是官方推荐的路径。
- 失败时提供尽可能多的信息:Google Test 不会在第一个断言失败处停下整个运行,而是中止当前测试、继续下一个测试,从而在一次"运行-编辑-编译"循环中定位多个 bug。
- 免除繁琐工作:框架自动跟踪所有已定义的测试,无需用户手动枚举。
- 快速:可以在多个测试间复用共享资源,只支付一次 setup/tear-down 成本,同时保持测试相互独立。
文档还指出 Google Test 基于经典的 xUnit 架构——用过 JUnit 或 PyUnit 的人会很快上手,否则大约 10 分钟即可掌握基础。
搭建新的测试工程:把 Google Test 编译成库并链接
Primer 给出的一步步流程是:
编译 Google Test 为库。官方为多种构建系统提供工程文件,均位于 gtest 根目录(本仓库中即 v8_6_7/testing/gtest/):
msvc/:Visual Studio 工程;xcode/:Mac Xcode 工程;make/:GNU make 的Makefile;codegear/:Borland C++ Builder;- autotools 脚本(已废弃)与
CMakeLists.txt(推荐)。
这些目录在仓库中均真实存在(msvc/、xcode/、make/、codegear/、CMakeLists.txt)。如果你的构建系统不在上述列表内,文档建议参考
make/Makefile了解编译方式——核心其实只有一条:以GTEST_ROOT和GTEST_ROOT/include作为头文件搜索路径,编译src/gtest-all.cc。本仓库中该文件位于 src/gtest-all.cc,它是一个把全部 gtest 实现汇总到单一编译单元的 amalgamation 文件,同目录还有 gtest_main.cc(官方main()实现)。为测试程序创建项目/构建目标:确保头文件搜索路径包含
GTEST_ROOT/include,这样编译器在编译测试代码时能找到"gtest/gtest.h"(本仓库中为 include/gtest/gtest.h)。让测试工程链接 Google Test 库。例如在 Visual Studio 中,就是对
gtest.vcproj添加一个依赖。如果仍有疑问,参考 Google Test 自身测试的构建方式——本仓库 test/ 目录下的
gtest_all_test.cc、gtest-message_test.cc等几十个测试文件就是现成范例。
基本概念:从断言到测试程序的分层结构
Primer 定义了四层结构,理解它们是读懂所有宏的前提:
- 断言(assertion):检查某条件是否为真的语句。断言结果分为三种:success(成功)、nonfatal failure(非致命失败)、fatal failure(致命失败)。发生致命失败时中止当前函数;非致命失败则程序照常继续。
- 测试(test):用断言验证被测代码的行为。若测试崩溃或存在失败断言,则测试失败,否则成功。
- 测试用例(test case):包含一个或多个测试。应把测试分组为反映被测代码结构的 test case;当多个测试需要共享对象和子例程时,放入测试固件类(test fixture)。
- 测试程序(test program):可包含多个 test case。
从源码结构看,这一分层直接映射到头文件组织:gtest.h 第 58–66 行通过#include聚合了gtest-death-test.h(death test)、gtest-param-test.h(参数化测试)、gtest-typed-test.h(类型化测试)、gtest-test-part.h(断言结果单元 TestPartResult)等子模块,形成 Primer 所说的统一入口gtest/gtest.h。
断言(Assertions):宏、失败消息与 ASSERT 和 EXPECT 的取舍
Google Test 的断言是形如函数调用的宏。断言失败时,框架打印断言所在的源文件与行号,外加失败消息;你还能追加自定义消息。
ASSERT_* 与 EXPECT_*:致命与非致命
断言成对出现,测试同一件事但效果不同:
ASSERT_*失败时产生致命失败并立即返回当前函数;EXPECT_*失败时产生非致命失败,函数继续执行。
通常优先使用EXPECT_*,因为它允许一个测试中报告多个失败;只有当"断言失败后继续执行毫无意义"时(例如后续代码要解引用一个可能为空的指针)才用ASSERT_*。
文档特别警告了一个微妙问题:由于失败的ASSERT_*会立即返回,可能跳过后面的清理代码,从而造成空间泄漏。因此如果你同时看到断言错误和堆检查器错误,要意识到二者可能同源。
自定义失败消息
用<<运算符把任意可流式输出到ostream的值串接进宏即可。原文示例:
ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length"; for (int i = 0; i < x.size(); ++i) { EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i; }C 字符串与string对象都支持;宽字符串(wchar_t*、WindowsUNICODE模式下的TCHAR*、std::wstring)在打印时会被转译为 UTF-8。在 gtest.h 中,EXPECT_EQ(val1, val2)正是通过内部比较宏展开实现这一行为的,失败时按需调用<<打印操作数。
基础布尔断言
| 致命断言 | 非致命断言 | 验证内容 |
|---|---|---|
ASSERT_TRUE(condition); | EXPECT_TRUE(condition); | condition 为真 |
ASSERT_FALSE(condition); | EXPECT_FALSE(condition); | condition 为假 |
无论哪种变体,断言失败都意味着其所在的测试失败。
二元比较断言
| 致命断言 | 非致命断言 | 验证内容 |
|---|---|---|
ASSERT_EQ(expected, actual); | EXPECT_EQ(expected, actual); | expected == actual |
ASSERT_NE(val1, val2); | EXPECT_NE(val1, val2); | val1 != val2 |
ASSERT_LT(val1, val2); | EXPECT_LT(val1, val2); | val1 < val2 |
ASSERT_LE(val1, val2); | EXPECT_LE(val1, val2); | val1 <= val2 |
ASSERT_GT(val1, val2); | EXPECT_GT(val1, val2); | val1 > val2 |
ASSERT_GE(val1, val2); | EXPECT_GE(val1, val2); | val1 >= val2 |
使用要点(原文逐条保留):
- 失败时 Google Test 会同时打印 val1 和 val2;
- 在
*_EQ*及后续所有相等性断言中,把待测表达式放在actual位置、期望值放在expected位置,因为失败消息按这一约定做了优化; - 参数必须可被断言的比较运算符比较,否则编译报错。自 v1.6.0 起参数不再强制支持
<<流式输出——支持则用它打印,不支持则框架尝试以最佳方式打印(进一步定制见同目录下的 Google Mock 文档); - 对用户自定义类型,定义了对应比较运算符后即可使用;定义了对应运算符时优先使用
ASSERT_*()宏,因为失败时不仅打印比较结果,还会打印两个操作数本身; - 参数恰好求值一次,带副作用的参数是安全的;但与其他 C/C++ 函数一样,求值顺序未定义,代码不得依赖特定顺序;
ASSERT_EQ()对指针做指针相等比较。若用于两个 C 字符串,比较的是"是否同一内存位置"而非内容;按值比较 C 字符串(如const char*)必须用ASSERT_STREQ();断言 C 字符串为NULL用ASSERT_STREQ(NULL, c_string);而比较两个string对象则用ASSERT_EQ;- 本节宏同时支持窄/宽字符串对象(
string与wstring)。
C 字符串比较断言
本组断言比较两个C 字符串;比较两个string对象请改用EXPECT_EQ/EXPECT_NE等。
| 致命断言 | 非致命断言 | 验证内容 |
|---|---|---|
ASSERT_STREQ(expected_str, actual_str); | EXPECT_STREQ(expected_str, actual_str); | 两个 C 字符串内容相同 |
ASSERT_STRNE(str1, str2); | EXPECT_STRNE(str1, str2); | 两个 C 字符串内容不同 |
ASSERT_STRCASEEQ(expected_str, actual_str); | EXPECT_STRCASEEQ(expected_str, actual_str); | 忽略大小写,内容相同 |
ASSERT_STRCASENE(str1, str2); | EXPECT_STRCASENE(str1, str2); | 忽略大小写,内容不同 |
注意命名中的 "CASE" 表示忽略大小写。*STREQ*和*STRNE*还接受宽 C 字符串(wchar_t*),宽字符串比较失败时其值以 UTF-8 窄字符串形式打印。NULL 指针与空字符串被视为不同。
更复杂的字符串技巧(子串、前后缀、正则匹配等)在 Primer 中指引到同目录的 Advanced Guide,本仓库中即 V1_7_AdvancedGuide.md。
简单测试:TEST() 宏与测试命名规则
创建测试三步走:
- 用
TEST()宏定义并命名一个测试函数(普通 C++ 函数,无返回值); - 在函数体内,除任意合法 C++ 语句外,用各种断言检查值;
- 结果由断言决定:任一断言失败(无论致命与否)或测试崩溃,则整个测试失败,否则成功。
TEST(test_case_name, test_name) { ... test body ... }规则:TEST()的参数从一般到具体——第一个参数是 test case 名,第二个是测试在该 case 内的名字;两者都必须是合法 C++ 标识符且不能包含下划线_;测试全名 = test case 名 + 个体名;不同 test case 中的测试可以同名。
Primer 中的示例:对整数阶乘函数int Factorial(int n);编写测试:
// Tests factorial of 0. TEST(FactorialTest, HandlesZeroInput) { EXPECT_EQ(1, Factorial(0)); } // Tests factorial of positive numbers. TEST(FactorialTest, HandlesPositiveInput) { EXPECT_EQ(1, Factorial(1)); EXPECT_EQ(2, Factorial(2)); EXPECT_EQ(6, Factorial(3)); EXPECT_EQ(40320, Factorial(8)); }Google Test 按 test case 分组测试结果,因此逻辑相关的测试应使用相同的第一个参数——上例中HandlesZeroInput与HandlesPositiveInput同属FactorialTest。
源码印证:测试是怎么"自动注册"的。"无需枚举即可运行全部测试"并非魔法,而是静态初始化。查看 gtest-internal.h,GTEST_TEST_宏展开后做了两件事:
- 以
test_case_name##_##test_name##_Test拼接生成一个继承自父类(::testing::Test)的测试类,测试体放在私有TestBody()中; - 生成一个静态成员
test_info_,其初始化器调用::testing::internal::MakeAndRegisterTestInfo(...),把 test case 名、测试名、CodeLocation(__FILE__, __LINE__)、SetUpTestCase/TearDownTestCase以及TestFactoryImpl<...>工厂对象登记进全局测试注册表。
也就是说,链接进可执行文件的每个TEST()都会在程序启动时通过静态对象构造器自行注册——这正是 Primer"框架自动跟踪所有测试"的实现基础,也解释了后文 Visual C++ 链接陷阱为何存在。
测试固件(Test Fixture):同一套数据配置服务多个测试
当你发现两个以上测试操作相似的数据时,就应使用 fixture 复用同一套对象配置。创建步骤(Primer 原文五步):
- 从
::testing::Test派生一个类,类体以protected:或public:开头(子类需要访问成员); - 在类中声明计划使用的对象;
- 如需要,编写默认构造函数或
SetUp()函数为每个测试准备对象。文档特别提醒一个高频笔误:SetUp()别写成小写 u 的Setup(); - 如需要,编写析构函数或
TearDown()释放SetUp()中分配的资源。何时用构造/析构、何时用SetUp()/TearDown(),Primer 指向同目录 FAQ(V1_7_FAQ.md)中的专门条目; - 如需要,为测试定义共享子例程。
使用 fixture 时改用TEST_F()(F 即 fixture)以访问固件中的对象与子例程:
TEST_F(test_case_name, test_name) { ... test body ... }TEST_F()的第一个参数必须是 fixture 类名。两点限制:C++ 宏系统无法让一个宏同时兼容两种测试,用错宏会直接编译报错;且必须先定义 fixture 类再在TEST_F()中使用,否则会收到virtual outside class declaration编译错误。
对每个TEST_F()测试,Google Test 按以下顺序执行(每个测试都拿到全新固件对象):
- 运行时新建一个测试固件对象;
- 立即调用
SetUp()初始化; - 运行测试体;
- 调用
TearDown()清理; - 删除固件对象。
同一 test case 的不同测试拥有不同的固件对象,且框架总在下个固件创建之前删除上一个,绝不复用;一个测试对固件的修改不会影响其他测试。
FIFO 队列的完整 Fixture 示例
Primer 用Queue类演示。接口:
template <typename E> // E is the element type. class Queue { public: Queue(); void Enqueue(const E& element); E* Dequeue(); // Returns NULL if the queue is empty. size_t size() const; ... };按惯例,fixture 命名为FooTest(Foo是被测类名):
class QueueTest : public ::testing::Test { protected: virtual void SetUp() { q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } // virtual void TearDown() {} Queue<int> q0_; Queue<int> q1_; Queue<int> q2_; };本例不需要TearDown(),因为析构函数已完成清理。随后用TEST_F()写测试:
TEST_F(QueueTest, IsEmptyInitially) { EXPECT_EQ(0, q0_.size()); } TEST_F(QueueTest, DequeueWorks) { int* n = q0_.Dequeue(); EXPECT_EQ(NULL, n); n = q1_.Dequeue(); ASSERT_TRUE(n != NULL); EXPECT_EQ(1, *n); EXPECT_EQ(0, q1_.size()); delete n; n = q2_.Dequeue(); ASSERT_TRUE(n != NULL); EXPECT_EQ(2, *n); EXPECT_EQ(1, q2_.size()); delete n; }注意ASSERT_TRUE(n != NULL)的使用动机:后续要解引用n,若其为NULL将段错误,失败后继续执行毫无意义,所以用ASSERT_*。经验法则:想继续暴露更多错误用EXPECT_*;失败后继续执行无意义用ASSERT_*。
实际执行时序:构造QueueTest对象t1→t1.SetUp()初始化 → 在t1上运行IsEmptyInitially→t1.TearDown()清理 → 析构t1;然后对另一个新对象重复上述步骤运行DequeueWorks。另有一个隐藏机制:Google Test 在测试对象构造时自动保存所有 Google Test 命令行标志的当前状态,并在析构时恢复,保证测试间互不干扰。
运行测试:RUN_ALL_TESTS() 的语义与约束
TEST()和TEST_F()会隐式注册测试(机制见上节MakeAndRegisterTestInfo),因此无需重新列举。定义完成后用RUN_ALL_TESTS()运行,全部成功返回0,否则返回1。它运行的是当前链接单元中的所有测试——可以来自不同 test case 甚至不同源文件。
RUN_ALL_TESTS()被调用时依次:保存所有 Google Test 标志状态 → 为第一个测试创建固件对象 →SetUp()初始化 → 在该对象上运行测试 →TearDown()清理 → 删除固件 → 恢复标志状态 → 对下一个测试重复上述步骤直至跑完。边界情况:若步骤 2(固件构造)产生致命失败,则步骤 3–5 直接跳过;若步骤 3(SetUp)致命失败,则步骤 4 被跳过。
两条硬性约束(原文 Important 级别):
- 不得忽略
RUN_ALL_TESTS()的返回值,否则gcc会报编译错误。设计动机是:自动化测试服务依据退出码(而非 stdout/stderr 输出)判定测试是否通过,因此main()必须return RUN_ALL_TESTS();。 - 只能调用一次。多次调用与部分高级特性(如线程安全的 death test)冲突,不被支持。
源码印证:在 gtest.h 中该宏定义为int RUN_ALL_TESTS() GTEST_MUST_USE_RESULT_;——GTEST_MUST_USE_RESULT_属性正是"忽略返回值即编译报错"的实现;第 2225 行注释亦明确要求在命令行经InitGoogleTest()解析之后才调用它。
编写 main():InitGoogleTest 与官方样板
Primer 提供的完整样板(原样保留,可直接复制使用):
#include "this/package/foo.h" #include "gtest/gtest.h" namespace { // The fixture for testing class Foo. class FooTest : public ::testing::Test { protected: // You can remove any or all of the following functions if its body // is empty. FooTest() { // You can do set-up work for each test here. } virtual ~FooTest() { // You can do clean-up work that doesn't throw exceptions here. } // If the constructor and destructor are not enough for setting up // and cleaning up each test, you can define the following methods: virtual void SetUp() { // Code here will be called immediately after the constructor (right // before each test). } virtual void TearDown() { // Code here will be called immediately after each test (right // before the destructor). } // Objects declared here can be used by all tests in the test case for Foo. }; // Tests that the Foo::Bar() method does Abc. TEST_F(FooTest, MethodBarDoesAbc) { const string input_filepath = "this/package/testdata/myinputfile.dat"; const string output_filepath = "this/package/testdata/myoutputfile.dat"; Foo f; EXPECT_EQ(0, f.Bar(input_filepath, output_filepath)); } // Tests that Foo does Xyz. TEST_F(FooTest, DoesXyz) { // Exercises the Xyz feature of Foo. } } // namespace int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }::testing::InitGoogleTest()解析命令行中的 Google Test 标志并移除已识别的项,使用户能通过命令行标志控制测试程序行为(这些标志在 gtest.h 中集中声明,包括--gtest_filter(按 glob 过滤测试)、--gtest_list_tests(只列出、不运行)、--gtest_output(XML 报告)、--gtest_break_on_failure、--gtest_color、--gtest_random_seed等,详解见 Advanced Guide)。必须在RUN_ALL_TESTS()之前调用它,否则标志无法正确初始化。在 Windows 上,InitGoogleTest()还有宽字符串重载,可服务于UNICODE模式编译的程序——对应源码为 gtest.h 中InitGoogleTest(int*, char**)与InitGoogleTest(int*, wchar_t**)两个声明。
嫌手写main()麻烦?直接把测试链接到gtest_main库即可。本仓库中其实现就是 src/gtest_main.cc。
Visual C++ 用户的重要注意事项
这是 Primer 中最长的一段实战警告,面向"测试放在库里、main()在别的库或 exe 里"的 MSVC 用户,值得完整继承:
问题:测试将不会被运行。原因是 Visual C++ 的一个已知链接器缺陷(微软反馈库 FeedbackID 244410):TEST()宏会创建用于注册测试的静态对象,这些对象虽无引用,但其构造函数仍应执行;而当 VC++ 链接器发现库中没有任何符号被引用时,会把整个库丢弃。
解决方案:
- 在测试库代码中声明一个导出函数,让主程序引用它,防止链接器丢库:
__declspec(dllexport) int PullInMyLibrary() { return 0; }- 在
main程序里调用它(通过静态变量的初始化触发):
int PullInMyLibrary(); static int dummy = PullInMyLibrary();若测试定义在静态库(非 DLL)中,则无需
__declspec(dllexport),但必须给主程序链接选项加/OPT:NOREF(MSVC IDE 中:.exe 工程属性 → Configuration Properties → Linker → Optimization,将 References 设为Keep Unreferenced Data (/OPT:NOREF)),防止链接器丢弃由测试生成的单个符号。还有一个坑:如果你把 Google Test 本身用作静态库(
gtest.vcproj中的默认定义方式),你的测试也必须放在静态库中;若测试必须放 DLL,则必须把 Google Test 也改成构建为 DLL,否则测试行为异常甚至完全不运行。
Primer 的总结论很直白:别把测试写在库里,让生命更简单。
继续学习与已知限制
下一步:Primer 指引读者阅读同目录的样例集(V1_7_Samples.md)与 Advanced Guide。本仓库中 samples/ 目录提供了 sample1–sample10 的完整可编译示例(含sample1_unittest.cc、sample10_unittest.cc等),test/ 目录则是框架自身的测试集,两者都是学习高级特性的最佳参照。
已知限制(线程安全):Google Test 设计上追求线程安全,其实现仅在pthreads库可用的系统上是线程安全的;在其他系统(如 Windows)上,从两个线程并发使用 Google Test 断言目前是不安全的。多数测试中这不是问题,因为断言通常发生在主线程。源码印证:gtest-port.h 用GTEST_IS_THREADSAFE宏汇总平台判定——该宏结合GTEST_HAS_PTHREAD(在 gtest-port.h 中按 Linux/Mac/HP-UX 等平台启发式定义,可用-DGTEST_HAS_PTHREAD=0显式关闭)等宏计算得出,正是 Primer 中"有 pthreads 即线程安全"这一声明的实现依据。有志者可志愿在gtest-port.h中为自己的平台实现所需的同步原语来贡献支持。
小结
回到 miniblink49 的工程语境:v8_6_7/testing/下的这份 gtest v1.7 是 V8 6.7 测试体系的组成部分,其目录布局(include/gtest/gtest.h公共 API +src/gtest-all.cc单文件实现 +gtest_main.cc默认 main +CMakeLists.txt/msvc/xcode多构建系统支持)恰好是 Primer 所述"编译成库再链接"流程的实体。掌握本篇内容后,你可以:
- 按"布尔 → 二元比较 → 字符串"三组断言宏正确选型(
EXPECT_*优先、必要时ASSERT_*、C 字符串用STREQ族); - 用
TEST()/TEST_F()+ fixture 组织反映被测结构的测试,理解每个测试获得全新固件对象的隔离语义; - 写出规范的
main()(InitGoogleTest→RUN_ALL_TESTS()且必须使用其返回值),或直接链接gtest_main; - 在 MSVC 环境下规避静态注册被链接器丢弃的经典陷阱(
PullInMyLibrary()技巧与/OPT:NOREF)。
【免费下载链接】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),仅供参考