ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

miniblink49 源码中的 Google Test 1.6 入门指南:用 C++ 测试框架武装你的浏览器内核项目

miniblink49 源码中的 Google Test 1.6 入门指南:用 C++ 测试框架武装你的浏览器内核项目 miniblink49 源码中的 Google Test 1.6 入门指南用 C 测试框架武装你的浏览器内核项目【免费下载链接】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是业界广泛使用的 C 单元测试框架它以 xUnit 架构为蓝本帮助开发者在 Linux、Windows、macOS 等平台上编写独立、可重复、可移植、快速的测试。本文以 miniblink49 仓库内随附的 v8_7_5/testing/gtest/docs/V1_6_Primer.md 为主体骨架结合仓库内真实存在的 gtest 源码与 V8 测试用例系统讲解 Google Test 的断言、测试、测试夹具、入口函数等核心概念让你能够在 miniblink49 这样的浏览器内核工程中熟练编写并运行自己的单元测试。读完本文你将掌握如何编译并链接 gtest 库、如何用TEST()/TEST_F()组织测试、如何使用EXPECT_*/ASSERT_*断言族精确校验行为、如何编写main()入口并通过命令行控制测试以及 Windows/MSVC 环境下需要注意的链接陷阱。为什么需要 Google Test好的测试应该具备哪些特质Google Test 的设计理念给出了六条明确答案测试应当独立且可重复independent and repeatable。一个测试因为其他测试而成功或失败是最难调试的场景。Google Test 通过为每个测试运行在不同的对象上来实现隔离当某个测试失败时你可以单独运行它进行快速调试。测试应当组织良好well organized并反映被测代码的结构。Google Test 将相关测试归入 test case测试用例test case 之间可以共享数据和子程序这种一致的模式让测试易于维护尤其方便开发者切换项目后快速上手新代码库。测试应当可移植且可复用portable and reusable。开源社区有大量平台无关的代码其测试也应平台无关。Google Test 支持多种操作系统与编译器gcc、MSVC 等支持或不支持异常的环境均可工作。测试失败时应提供尽可能多的信息。Google Test 不会在第一个失败处停下而是只终止当前测试、继续运行下一个你还可以使用非致命失败nonfatal failure让当前测试继续执行从而在单次“运行—修改—编译”循环中发现并修复多个 bug。框架应当把测试作者从繁琐事务中解放出来让其专注于测试内容。Google Test 自动跟踪所有已定义的测试无需手动枚举。测试应当快速fast。你可以跨测试复用共享资源只付出一次 set-up/tear-down 的开销而不必让测试相互依赖。由于 Google Test 基于流行的 xUnit 架构如果你用过 JUnit 或 PyUnit上手会非常快即便从未接触过也只需约 10 分钟即可掌握基础并开始使用。在 miniblink49 仓库中定位 Google Testminiblink49 仓库将 Google Test 与 Google Mock 一同内嵌在 v8_7_5/testing/ 目录下同时存在gtest/与gmock/两个子目录并随附了从 v1.5 到 v1.7 的系列文档本文讲解的 V1_6_Primer.md 即为其入门手册。仓库内的 gtest 目录结构如下include/gtest/公共头文件测试代码通过#include gtest/gtest.h引入src/核心实现源码包括gtest-all.cc、gtest.cc、gtest_main.cc等make/、msvc/、xcode/、codegear/分别对应 GNU make、Visual Studio、Xcode、Borland C Builder 的构建文件test/gtest 自身的单元测试可作为编写测试的最佳范例samples/十个官方示例sample1 到 sample10。更重要的是这个 gtest 并非“摆设”V8 引擎miniblink49 的核心 JS 引擎之一的单元测试就大量构建在它之上。例如 v8_7_5/test/unittests/zone/zone-unittest.cc 中直接使用了TEST(Zone, 8ByteAlignment)与ASSERT_EQ来验证 Zone 分配器的字节对齐行为#include src/zone/zone.h #include testing/gtest/include/gtest/gtest.h TEST(Zone, 8ByteAlignment) { AccountingAllocator allocator; Zone zone(allocator, ZONE_NAME); for (size_t i 0; i 16; i) { ASSERT_EQ(reinterpret_castintptr_t(zone.New(i)) % 8, 0); } }可见gtest 在 miniblink49 / V8 工程中就是标准测试基础设施理解它的用法等同于直接读懂 V8 数千个测试用例。搭建一个新的测试工程要使用 Google Test 编写测试程序需要先把 gtest 编译成库再让测试程序链接它。仓库在 gtest 根目录v8_7_5/testing/gtest/为多种构建系统提供了构建文件构建系统目录/文件适用平台Visual Studiomsvc/gtest.vcproj等WindowsXcodexcode/macOSGNU makemake/MakefileLinux/UnixBorland C Buildercodegear/WindowsCMake推荐CMakeLists.txt跨平台如果你的构建系统不在上述列表中可以参照 make/Makefile 了解 gtest 的编译方式核心做法是编译 src/gtest-all.cc并在头文件搜索路径中加入GTEST_ROOT与GTEST_ROOT/include其中GTEST_ROOT指 gtest 根目录。以仓库内的路径为例即把v8_7_5/testing/gtest与v8_7_5/testing/gtest/include加入头文件搜索路径。编译出 gtest 库之后为你的测试程序创建项目或构建目标并确保编译测试代码时GTEST_ROOT/include在头文件搜索路径中使编译器能找到gtest/gtest.h链接阶段与 gtest 库连接例如在 Visual Studio 中为测试工程添加对gtest.vcproj的依赖。如果仍有疑问最好的办法是直接查看 gtest 自身的测试是如何构建和使用的——仓库 v8_7_5/testing/gtest/test/ 目录下有大量现成范例。基本概念使用 Google Test 时从编写**断言assertion**开始——断言是检查某个条件是否为真的语句。断言的结果有三种成功success、非致命失败nonfatal failure、致命失败fatal failure。一旦发生致命失败会中止当前函数否则程序继续正常运行。测试test用断言来验证被测代码的行为。如果测试崩溃或存在失败的断言则该测试失败否则成功。测试用例test case包含一个或多个测试。你应当把测试按被测代码的结构组织进 test case。测试夹具test fixture当同一个 test case 中的多个测试需要共享公共对象和子程序时把它们放进一个 fixture 类。测试程序test program可以包含多个 test case。下面从最基础的断言讲起逐步构建出完整的测试程序。断言AssertionsGoogle Test 的断言是形似函数调用的宏。当断言失败时Google Test 会打印断言所在源文件与行号以及失败消息你也可以通过流式运算符附加自定义失败消息。断言成对出现两者测试相同的内容但对当前函数的影响不同ASSERT_*版本失败时产生致命失败并中止当前函数EXPECT_*版本失败时产生非致命失败不会中止当前函数。通常更推荐EXPECT_*因为它允许在一个测试中报告多个失败但当失败后继续执行没有意义时例如后续代码要解引用一个可能为NULL的指针应当使用ASSERT_*。需要留意的是ASSERT_*失败会立即从当前函数返回可能跳过其后的清理代码从而造成空间泄漏——如果你在断言错误之外还收到堆检查器heap checker的报错请把这一点纳入考虑。自定义失败消息只需用运算符或一连串把内容流进宏即可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_TRUE(condition);EXPECT_TRUE(condition);condition 为真ASSERT_FALSE(condition);EXPECT_FALSE(condition);condition 为假记住失败时ASSERT_*产生致命失败并从当前函数返回EXPECT_*产生非致命失败、函数继续执行。无论哪种断言失败都意味着其所在测试失败。可用平台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失败时Google Test 会同时打印val1和val2两个值。在使用ASSERT_EQ*/EXPECT_EQ*以及后续介绍的所有相等性断言时建议把被测表达式放在 actual 位置、期望值放在 expected 位置因为 Google Test 的失败消息针对这一惯例做了优化。几个重要细节值参数必须能被断言对应的比较运算符比较否则会得到编译错误。v1.6.0 起参数不再强制要求支持运算符若支持失败时会调用它打印参数否则 Google Test 会尽力以最佳方式打印关于如何自定义打印行为可参考 gmock 的 CookBook 中的 “Teaching Google Mock how to print your values” 一节。这些断言可用于自定义类型但前提是你为该类型定义了相应的比较运算符如、。若运算符已定义优先使用ASSERT_*()宏因为它们不仅打印比较结果还打印两个操作数。参数总是只求值一次因此参数带有副作用是安全的但与其他 C/C 函数一样参数求值顺序未定义代码不应依赖特定求值顺序。ASSERT_EQ()对指针做的是指针相等比较如果用于两个 C 字符串它检查的是二者是否位于同一内存位置而非内容是否相同。因此比较 C 字符串如const char*内容请用稍后介绍的ASSERT_STREQ()特别地断言某个 C 字符串为NULL应使用ASSERT_STREQ(NULL, c_string)。而两个string对象的比较则应当使用ASSERT_EQ。本组宏同时支持窄字符串和宽字符串对象string与wstring。可用平台Linux、Windows、Mac。字符串比较断言本组断言比较两个C 字符串const char*若要比较两个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);两个 C 字符串内容相同忽略大小写ASSERT_STRCASENE(str1, str2);EXPECT_STRCASENE(str1, str2);两个 C 字符串内容不同忽略大小写注意断言名中的 “CASE” 表示忽略大小写。*STREQ*与*STRNE*也接受宽 C 字符串wchar_t*若宽字符串比较失败其值会以 UTF-8 窄字符串形式打印。此外NULL指针与空字符串被视为不同。可用平台Linux、Windows、Mac。更多字符串比较技巧子串、前缀、后缀、正则匹配等可参见 V1_6_AdvancedGuide.md。简单测试TEST() 宏创建测试只需三步用TEST()宏定义并命名一个测试函数——它们是普通的、不返回值的 C 函数在函数体内写入任意合法的 C 语句并用各种 Google Test 断言检查数值测试结果由断言决定只要有一个断言失败无论致命还是非致命或测试崩溃整个测试就失败否则成功。基本形式TEST(test_case_name, test_name) { ... test body ... }TEST()的两个参数从一般到具体排列第一个参数是 test case 名第二个是该 test case 内的测试名。两个名字都必须是合法的 C 标识符且不应包含下划线_。测试的全名由所属 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都属于 test caseFactorialTest。可用平台Linux、Windows、Mac。测试夹具让多个测试共享同一份数据配置当你发现两个或多个测试操作相似的数据时可以使用测试夹具test fixture从而为多个测试复用同一份对象配置。创建夹具的步骤从::testing::Test派生一个类类体以protected:或public:开头因为子类需要访问夹具成员在类中声明计划使用的对象如有必要编写默认构造函数或SetUp()函数为每个测试准备对象。常见错误是把SetUp()误拼成Setup()小写 u请务必避免如有必要编写析构函数或TearDown()函数释放SetUp()中分配的资源何时用构造/析构、何时用SetUp()/TearDown()参见 V1_6_FAQ.md 中 “Should I use the constructor/destructor of the test fixture or the set-up/tear-down function” 条目如有需要为测试定义可共享的子程序。使用夹具时用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 中不同测试拥有不同的夹具对象Google Test 总是在创建下一个夹具之前删除上一个不会为多个测试复用同一个夹具——一个测试对夹具的任何修改都不会影响其他测试。Queue 示例以 FIFO 队列类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; ... };首先定义夹具类。按惯例被测类是Foo时夹具命名为FooTestclass 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_*。例如Dequeue测试中的ASSERT_TRUE(n ! NULL)因为后面要解引用指针n若n为NULL将导致段错误。这些测试运行时实际发生的过程是Google Test 构造一个QueueTest对象记为t1t1.SetUp()初始化t1第一个测试IsEmptyInitially在t1上运行t1.TearDown()在测试结束后清理t1被析构对另一个QueueTest对象重复以上步骤这次运行DequeueWorks测试。可用平台Linux、Windows、Mac。另外Google Test 会在测试对象构造时自动保存所有 gtest 标志并在析构时恢复它们。调用测试RUN_ALL_TESTS()TEST()和TEST_F()会隐式地将测试注册到 Google Test——与其他许多 C 测试框架不同你无需为了运行测试而重新罗列所有已定义的测试这一自动注册机制的宏定义可在 gtest.h 中看到GTEST_TEST最终展开为内部实现GTEST_TEST_。定义完测试后用RUN_ALL_TESTS()运行它们所有测试成功时返回0否则返回1。注意它运行的是链接单元中的全部测试——可以来自不同 test case甚至不同源文件。调用RUN_ALL_TESTS()时宏会保存所有 Google Test 标志的状态为第一个测试创建夹具对象通过SetUp()初始化在夹具对象上运行测试通过TearDown()清理删除夹具恢复所有 Google Test 标志的状态对下一个测试重复以上步骤直到全部测试运行完毕。此外如果第 2 步中夹具的构造函数产生了致命失败第 3–5 步没有意义因而被跳过类似地若第 3 步产生致命失败第 4 步也会被跳过。重要绝不能忽略RUN_ALL_TESTS()的返回值否则 gcc 会给出编译错误。这一设计的理由是自动化测试服务依据退出码而非 stdout/stderr 输出判定测试是否通过因此你的main()必须返回RUN_ALL_TESTS()的值。同时RUN_ALL_TESTS()只能调用一次。多次调用会与某些高级特性如线程安全的 death tests冲突因而不被支持。可用平台Linux、Windows、Mac。编写 main() 函数可以从下面的样板代码开始#include this/package/foo.h #include gtest/gtest.h namespace { // The fixture for testing class Foo. class FooTest : public ::testing::Test { protected: // 以下函数体为空时可以移除其中任意一个或全部。 FooTest() { // 可以在这里为每个测试做 set-up 工作。 } virtual ~FooTest() { // 可以在这里做不抛异常的清理工作。 } // 如果构造函数和析构函数不足以完成每个测试的 // set-up / clean-up可以定义下面两个方法 virtual void SetUp() { // 这里的代码将在构造函数之后、每个测试之前立即被调用。 } virtual void TearDown() { // 这里的代码将在每个测试之后、析构函数之前立即被调用。 } // 这里声明的对象可被 Foo 的 test case 中的所有测试使用。 }; // 测试 Foo::Bar() 方法是否做了 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)); } // 测试 Foo 是否做了 Xyz。 TEST_F(FooTest, DoesXyz) { // 演练 Foo 的 Xyz 特性。 } } // namespace int main(int argc, char **argv) { ::testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }::testing::InitGoogleTest()负责解析命令行中的 Google Test 标志并移除所有已识别的标志从而允许用户通过各种命令行标志控制测试程序的行为详见 V1_6_AdvancedGuide.md。必须在调用RUN_ALL_TESTS()之前调用它否则标志无法被正确初始化。在 Windows 上InitGoogleTest()同样支持宽字符串因此可用于以UNICODE模式编译的程序。如果你觉得为每个测试程序手写main()太麻烦Google Test 已经提供了基础实现直接链接gtest_main 库即可。仓库中的 src/gtest_main.cc 就是该实现的真身它打印一行提示后调用InitGoogleTest并返回RUN_ALL_TESTS()的结果GTEST_API_ int main(int argc, char **argv) { printf(Running main() from gtest_main.cc\n); testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }在 miniblink49 / V8 的测试实践中两种方式都有体现既有像 zone-unittest.cc 这样只写测试、由构建系统统一提供入口的用法也有 gtest 自身 test/ 目录下大量自备main()的测试程序。Visual C 用户的重要注意事项如果你把测试放入库中而main()函数在另一个库或 .exe 中这些测试将不会运行——这是 Visual C 链接器的一个已知缺陷。原因是定义测试时 Google Test 会创建若干用于注册测试的静态对象这些对象不被其他代码引用但其构造函数仍应执行当 Visual C 链接器发现库中没有任何内容被外部引用时就会把整个库丢弃。解决办法是让主程序显式引用测试库防止链接器丢弃它。在测试库代码中声明一个函数__declspec(dllexport) int PullInMyLibrary() { return 0; }如果测试放在静态库而非 DLL中则不需要__declspec(dllexport)。然后在主程序中调用该函数int PullInMyLibrary(); static int dummy PullInMyLibrary();这样会让测试保持被引用状态并在启动时完成自我注册。此外如果在静态库中定义测试请给主程序的链接器选项加上/OPT:NOREF。在 MSVC IDE 中打开 .exe 项目的“属性 → 配置属性 → 链接器 → 优化”将 References 设置为Keep Unreferenced Data (/OPT:NOREF)防止链接器从最终可执行文件中丢弃测试生成的独立符号。还有一个陷阱如果把 Google Test 用作静态库gtest.vcproj默认如此那么你的测试也必须放在静态库中如果测试必须在 DLL 中就必须把 Google Test 也改成编译成 DLL否则测试无法正确运行、甚至完全不会运行。结论是别把测试写进库中让生活简单些。已知限制Google Test 被设计为线程安全的但在提供pthreads库的系统上实现才是真正线程安全的在其他系统例如 Windows上目前从两个线程并发使用 Google Test 断言是不安全的。大多数测试中这不成问题因为断言通常在主线程执行。如果你愿意贡献代码可以为自己的平台在gtest-port.h中实现必要的同步原语。下一步学习路径至此你已经掌握了 Google Test 的全部基础知识可以开始编写并运行自己的测试了。继续深入可参考V1_6_Samples.md官方示例仓库 samples/ 目录下有 sample1 至 sample10 的完整代码V1_6_AdvancedGuide.md更多高级特性包括命令行标志、死亡测试、参数化测试、类型参数化测试等在 miniblink49 仓库中v8_7_5/test/unittests/ 下的 V8 单元测试如 zone、heap、compiler-dispatcher 等模块是 gtest 在真实大型内核工程中的最佳范本直接阅读即可看到TEST、TEST_F、EXPECT_*、ASSERT_*在复杂场景下的组合用法。【免费下载链接】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),仅供参考
返回列表