使用 Google C++ Testing Framework(gtest 1.7)编写 C++ 单元测试:从 Primer 到 miniblink49 源码实践

发布时间:2026/9/17 13:47:30
使用 Google C++ Testing Framework(gtest 1.7)编写 C++ 单元测试:从 Primer 到 miniblink49 源码实践 使用 Google C Testing Frameworkgtest 1.7编写 C 单元测试从 Primer 到 miniblink49 源码实践【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49Google C Testing Framework简称 Google Test 或 gtest是一套基于 xUnit 架构的 C 单元测试框架帮助开发者编写独立、可重复、可移植且信息丰富的测试。本仓库以 V8 5.7 的testing/gtest目录完整携带了 gtest 1.7 的文档、源码与示例并被 ginV8 绑定层等多个模块的测试代码实际使用。读完本文你将掌握 gtest 的断言体系、TEST/TEST_F测试组织方式、main()入口写法与多平台构建方法并能结合本仓库源码理解其底层实现。为什么选择 Google Test优秀测试的标准Google Test 的设计哲学建立在六条测试准则之上这也是判断好测试的通用标准测试应当独立且可重复independent and repeatableGoogle Test 为每个测试运行在不同的对象上隔离彼此影响某个测试失败时可单独运行它以快速调试。测试应当组织良好并反映被测代码结构organized相关测试被归入同一 test case可共享数据与子程序模式统一、易于维护尤其在跨项目协作时价值突出。测试应当可移植且可复用portable and reusableGoogle Test 支持 Linux、Windows、Mac 多种操作系统支持 gcc、MSVC 等多种编译器支持或不支持异常的环境均可使用。注意当前 1.7 发布版仅内置 Linux 构建脚本其他平台脚本仍在完善中。失败时应提供尽可能多的信息Google Test 不会在第一个失败处停止而是只中断当前测试并继续执行下一个非致命失败nonfatal failure后当前测试仍可继续因此一次运行-编辑-编译循环能发现并修复多个 bug。解放测试编写者的杂务框架自动跟踪所有已定义的测试无需手动枚举。测试应当快可跨测试复用共享资源set-up/tear-down 只支付一次成本同时不使测试相互依赖。由于基于流行的 xUnit 架构使用过 JUnit 或 PyUnit 的开发者几乎可以无缝上手新手通常 10 分钟即可掌握基础。本仓库的 gtest 实现即位于 v8_5_7/testing/gtest其文档体系包括 V1_7_Primer.md本文对应入门指南、V1_7_AdvancedGuide.md进阶指南、V1_7_FAQ.md 与 V1_7_Samples.md。搭建新的测试项目构建 Google Test 库编写测试程序前需要先把 Google Test 编译成库并链接进测试。仓库在 gtest 根目录v8_5_7/testing/gtest提供了多种主流构建系统的构建文件构建系统位置/文件说明Visual Studiomsvc/直接使用gtest.vcproj工程Xcode (Mac)xcode/提供 Xcode 工程GNU makemake/经典make/MakefileBorland C Buildercodegear/对应工程文件CMake推荐CMakeLists.txt跨平台首选如果你的构建系统不在此列可以阅读 make/Makefile 了解 Google Test 的编译方式本质上只需编译 src/gtest-all.cc并把GTEST_ROOT与GTEST_ROOT/include加入头文件搜索路径GTEST_ROOT即 gtest 根目录。该文件是伞形编译单元一次性包含 gtest 的各实现文件例如 gtest.cc框架核心、gtest-death-test.cc死亡测试、gtest-typed-test.cc类型参数化测试等。配置测试程序编译好 gtest 库后为测试程序创建独立项目/构建目标并确保头文件搜索路径包含GTEST_ROOT/include使编译器能找到gtest/gtest.h测试程序链接 Google Test 库Visual Studio 中即添加对gtest.vcproj的依赖。本仓库中 gtest 1.7 的头文件体系位于 v8_5_7/testing/gtest/include/gtest入口 gtest.h 对外暴露断言、TEST/TEST_F、RUN_ALL_TESTS()等全部核心 API内部实现分布在gtest-message.h、gtest-printers.h、gtest-typed-test.h、gtest-spi.h等文件中。如果仍有疑问可以直接参考 gtest 自带测试v8_5_7/testing/gtest/test与示例v8_5_7/testing/gtest/samples的构建方式。基本概念使用 Google Test 时测试代码由三个层次组成断言Assertions检查某个条件是否为真的语句。断言的结果分为成功success、非致命失败nonfatal failure与致命失败fatal failure三种。致命失败会中止当前函数否则程序继续正常执行。测试Tests用断言验证被测代码的行为。若测试崩溃或有断言失败则测试失败否则成功。测试用例Test Case包含一个或多个测试。应将测试按被测代码的结构组织进 test case当多个测试需要共享对象与子程序时把它们放入**测试夹具test fixture**类。一个**测试程序test program**可以包含多个 test case。编写测试程序从单个断言开始逐级构建到测试与 test case。断言体系Google Test 的断言是形似函数调用的宏。断言失败时框架打印断言的源文件与行号以及失败信息也可用流式操作符追加自定义失败信息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; }任何可被ostream输出的对象都能流式写入断言宏包括 C 字符串与string对象。宽字符串wchar_t*、Windows UNICODE 模式下的TCHAR*、std::wstring输出时会被转换为 UTF-8。致命与非致命断言的选择断言总是成对出现ASSERT_*失败时产生致命失败并中止当前函数EXPECT_*失败时产生非致命失败不中止函数。通常优先使用EXPECT_*以便单次测试报告多个失败但当失败后继续执行没有意义时应使用ASSERT_*。注意失败的ASSERT_*会立即从当前函数返回可能跳过其后的清理代码从而造成空间泄漏——如果除断言错误外还出现堆检查器heap checker报错请留意这一点。基础断言致命断言非致命断言验证内容ASSERT_TRUE(condition);EXPECT_TRUE(condition);condition 为真ASSERT_FALSE(condition);EXPECT_FALSE(condition);condition 为假两种断言失败都会导致所在测试失败。可用平台Linux、Windows、Mac。二元比较断言致命断言非致命断言验证内容ASSERT_EQ(expected, actual);EXPECT_EQ(expected, actual);expectedactualASSERT_NE(val1, val2);EXPECT_NE(val1, val2);val1!val2ASSERT_LT(val1, val2);EXPECT_LT(val1, val2);val1val2ASSERT_LE(val1, val2);EXPECT_LE(val1, val2);val1val2ASSERT_GT(val1, val2);EXPECT_GT(val1, val2);val1val2ASSERT_GE(val1, val2);EXPECT_GE(val1, val2);val1val2使用要点参数顺序失败时框架会同时打印两个操作数。为使失败信息可读把被测表达式放在actual位置、期望值放在expected位置Google Test 的失败信息为此约定做了优化。可比较性值参数必须支持对应比较运算符否则编译报错。自 1.6.0 起不再强制要求参数支持流输出若支持失败时会调用它打印参数否则框架会尝试以最佳方式打印。自定义类型只要定义了对应的比较运算符如、这些断言即可用于用户自定义类型且会同时打印比较结果与两个操作数。求值次数参数总是恰好求值一次因此允许带副作用但参数求值顺序与普通 C/C 函数一样未定义代码不应依赖任何特定顺序。指针与字符串ASSERT_EQ()对指针做指针相等比较。用于两个 C 字符串时它比较的是内存地址而非值比较 C 字符串内容应使用下文ASSERT_STREQ()断言 C 字符串为NULL应用ASSERT_STREQ(NULL, c_string)而比较两个string对象应使用ASSERT_EQ。本节的宏同时支持窄/宽字符串对象string与wstring。字符串比较断言以下断言比较两个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指针与空字符串被视为不同。子串、前缀、后缀与正则匹配等更多字符串技巧见 V1_7_AdvancedGuide.md。简单测试TEST 宏创建一个测试只需三步用TEST()宏定义并命名一个测试函数普通 C 函数不返回值在函数体内用各种 gtest 断言检查值测试结果由断言决定任一断言失败致命或非致命或测试崩溃整个测试失败否则成功。基本形式TEST(test_case_name, test_name) { ... test body ... }TEST()的两个参数从一般到具体第一个是 test case 名称第二个是该 test case 内的测试名。两者都必须是合法 C 标识符且不应包含下划线_。测试的**全名full name**由 test case 名与测试名组成不同 test case 中的测试可以使用相同的测试名。以阶乘函数为例int Factorial(int n); // Returns the factorial of 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 分组展示结果因此逻辑相关的测试应放在同一 test case即TEST()第一个参数相同。上述示例中HandlesZeroInput与HandlesPositiveInput同属FactorialTest。本仓库的官方示例 sample1_unittest.cc 完整演示了这一模式其中TEST(FactorialTest, Negative)、TEST(FactorialTest, Zero)等分别覆盖负数、零与正数输入且明确注明gtest 保证每个测试恰好运行一次但不保证执行顺序因此测试结果不能依赖顺序。测试夹具为多个测试复用同一份数据配置当两个或更多测试基于相似数据运行时可使用**测试夹具test fixture**复用同一套对象配置。创建夹具从::testing::Test派生类以protected:或public:开头子类需要访问夹具成员在类中声明计划使用的对象如需要编写默认构造函数或SetUp()函数为每个测试准备对象常见错误是把SetUp()误拼成小写 u 的Setup()如需要编写析构函数或TearDown()释放SetUp()中分配的资源。何时用构造函数/析构函数、何时用SetUp()/TearDown()见 V1_7_FAQ.md 对应条目如需要定义测试间共享的子程序。使用夹具时改用TEST_F()而非TEST()以访问夹具中的对象与子程序TEST_F(test_case_name, test_name) { ... test body ... }与TEST()相同第一个参数是 test case 名但对TEST_F()而言它必须是测试夹具类的名字_F即 fixture。由于 C 宏系统无法用一个宏同时处理两类测试用错宏会产生编译错误同时必须先定义夹具类再用于TEST_F()否则会得到 virtual outside class declaration 编译错误。对每个用TEST_F()定义的测试Google Test 会在运行时创建一个全新的测试夹具对象立即通过SetUp()初始化它运行测试调用TearDown()清理删除夹具对象。同一 test case 中不同测试拥有不同的夹具对象gtest 总是在创建下一个前删除前一个绝不跨测试复用夹具因此一个测试对夹具的修改不会影响其他测试。队列测试实例假设被测的 FIFO 队列类接口如下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; ... };按惯例夹具命名为FooTestFoo为被测类名class QueueTest : public ::testing::Test { protected: virtual void SetUp() { q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } // virtual void TearDown() {} Queueint q0_; Queueint q1_; Queueint 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_*与EXPECT_*的使用规则希望失败后继续暴露更多错误时用EXPECT_*失败后继续没有意义时用ASSERT_*。例如DequeueWorks中第二个断言用ASSERT_TRUE(n ! NULL)因为随后要解引用指针n若n为NULL将导致段错误segfault。这些测试运行时发生的过程构造QueueTest对象t1→t1.SetUp()初始化 → 在t1上运行第一个测试 →t1.TearDown()清理 → 析构t1→ 对另一个QueueTest对象重复上述步骤并运行第二个测试。另外Google Test 会在测试对象构造时自动保存所有 gtest 标志析构时恢复。调用测试RUN_ALL_TESTSTEST()与TEST_F()会隐式地向 Google Test 注册测试因此无需像许多其他框架那样手动罗列测试。定义完成后用RUN_ALL_TESTS()运行全部测试全部成功返回0否则返回1。它运行链接单元中的所有测试——可以来自不同 test case甚至不同源文件。RUN_ALL_TESTS()宏的执行流程保存所有 gtest 标志状态为第一个测试创建夹具对象通过SetUp()初始化在夹具对象上运行测试通过TearDown()清理删除夹具恢复所有 gtest 标志状态对下一个测试重复以上步骤直至全部运行完毕。若第 2 步构造函数产生致命失败第 3~5 步无意义会被跳过若第 3 步SetUp()产生致命失败第 4 步会被跳过。重要注意事项不可忽略返回值忽略RUN_ALL_TESTS()的返回值会导致 gcc 编译错误。原因在于自动化测试服务依据退出码而非 stdout/stderr 输出判断测试是否通过因此main()必须返回RUN_ALL_TESTS()的值。只能调用一次多次调用会与某些高级特性如线程安全的死亡测试冲突不受支持。编写 main() 函数官方推荐的样板代码#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 doesnt 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 标志并移除所有已识别标志从而允许用户通过各类标志控制测试程序行为详见 V1_7_AdvancedGuide.md。必须在调用RUN_ALL_TESTS()之前调用它否则标志无法正确初始化。在 Windows 上InitGoogleTest()同样支持宽字符串可用于UNICODE模式下编译的程序。如果不想每次手写main()Google Test 提供了基本实现将测试与 gtest_main 库链接即可。本仓库的 src/gtest_main.cc 正是该实现——它打印Running main() from gtest_main.cc然后依次调用testing::InitGoogleTest(argc, argv)并return RUN_ALL_TESTS();源码直观印证了上述样板代码的每个环节。Visual C 用户的重要提示若把测试放进库而main()在另一个库或 .exe 中测试将不会运行——这是 Visual C 链接器的已知缺陷。定义测试时 gtest 会创建用于注册的静态对象这些对象不被其他代码引用但构造函数仍需执行当链接器发现库中没有任何东西被外部引用时会把整个库丢弃。解决方法在含测试的库代码中声明一个函数__declspec(dllexport) int PullInMyLibrary() { return 0; }若测试放在静态库而非 DLL 中则不需要__declspec(dllexport)。 2. 在 main 程序中调用它强制引用该库int PullInMyLibrary(); static int dummy PullInMyLibrary();这会使测试保持被引用并在启动时完成注册。此外若测试定义在静态库中需为主程序链接器添加/OPT:NOREFMSVC IDE 中位于 .exe 项目属性 → Configuration Properties → Linker → Optimization将 References 设为Keep Unreferenced Data (/OPT:NOREF)防止链接器丢弃测试符号。还有一处陷阱若以静态库方式使用 Google Test如gtest.vcproj的默认定义测试也必须位于静态库中若必须在 DLL 中则必须同时把 Google Test 构建为 DLL否则测试不会正确运行甚至完全不运行。总体结论别把测试写进库这会让生活简单得多。结合仓库源码的实践印证gin 模块gtest 在 V8 绑定层的真实用例本仓库的 ginV8 集成层大量使用 gtest 1.7 编写单元测试是入门指南的最佳实战注脚。典型文件gin/converter_unittest.cc测试 V8 值与 C 类型转换器gin/interceptor_unittest.cc测试属性拦截器gin/wrappable_unittest.cc测试 C 对象包装为 JS 可调用对象其中MyObject继承WrappableMyObject通过ObjectTemplateBuilder暴露value属性gin/shell/gin_shell_unittest.cc 与 gin/shell_runner_unittest.cc测试 shell 运行器。这些测试统一#include testing/gtest/include/gtest/gtest.h并大量使用TEST_F、EXPECT_EQ、EXPECT_TRUE等断言。夹具的实际形态gin::V8Testgin/test/v8_test.h 中的V8Test类是对夹具模式的直接应用class V8Test : public testing::Test { public: V8Test(); ~V8Test() override; void SetUp() override; void TearDown() override; protected: base::MessageLoop message_loop_; scoped_ptrIsolateHolder instance_; v8::Persistentv8::Context context_; ... };它继承testing::Test重写SetUp()/TearDown()在 protected 区声明IsolateHolder、V8Context等成员供所有继承它的测试复用。这正是 Primer 所描述的为多个测试复用同一数据配置的模板式用法V8 隔离区isolate的创建与销毁等昂贵且易错的准备工作被收敛进夹具测试只关心业务断言。断言宏的 JS 桥接gin::GTest更有意思的是gin 甚至把 gtest 断言桥接到了 JavaScript。gin/test/gtest.cc 实现gin::GTest模块它通过ObjectTemplateBuilder暴露fail、expectTrue、expectFalse、expectEqual四个方法底层分别调用FAIL()、EXPECT_TRUE()、EXPECT_FALSE()与EXPECT_TRUE(expected-StrictEquals(actual))。也就是说在 gin 的 JS 测试脚本中可以写gtest.expectEqual(a, b)来获得与 C 侧一致的断言语义——gtest 的断言哲学被无缝延伸到 V8 的 JavaScript 测试环境中。配套的 gin/test/file_runner.cc、gin/test/run_js_tests.cc 负责加载并执行 JS 测试用例。官方示例与文档入口进一步学习可参考示例代码v8_5_7/testing/gtest/samplessample1_unittest.cc起共 10 个示例覆盖从基础TEST到类型参数化、值参数化等高级主题入门指南docs/V1_7_Primer.md本文所述内容与当前版 docs/Primer.md进阶指南docs/V1_7_AdvancedGuide.md样例导读docs/V1_7_Samples.md常见问题docs/V1_7_FAQ.md。已知限制Google Test 在设计上追求线程安全在有pthreads库的系统上实现是线程安全的但在其他系统如 Windows上当前从两个线程并发使用 gtest 断言是不安全的。多数测试不受影响因为断言通常在主线程完成。若想贡献代码可以在 gtest-port.h 中为你的平台实现所需的同步原语。小结从断言体系到TEST/TEST_F组织方式再到RUN_ALL_TESTS()与main()入口gtest 1.7 的入门路径清晰且完整。在本仓库中这套框架不是孤立的第三方代码gin 模块的 C 单元测试、gin::V8Test夹具以及gin::GTest的 JS 断言桥接都建立在 v8_5_7/testing/gtest 之上。掌握了本文的基础你就可以对照 V1_7_AdvancedGuide.md 继续探索死亡测试、值参数化、类型参数化等进阶能力并将这套方法论应用到浏览器内核其他模块的测试编写中。【免费下载链接】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),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询