ARTICLE DETAIL

资讯详情

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

从零上手 gMock:C++ 单元测试中的 Mock 对象与交互验证完全指南

从零上手 gMock:C++ 单元测试中的 Mock 对象与交互验证完全指南 性能剖析内存管理开发工具【免费下载链接】gperftoolsMain gperftools repository项目地址https://gitcode.com/gh_mirrors/gp/gperftools点击查看免费下载gMockGoogle Mock是随 googletest 一起分发的 C Mock 框架它让你无需手写桩代码就能通过简单宏生成 mock 类、用类英文的 DSL 语法设定期望并自动验证被测代码与外部依赖之间的调用交互。本文以 gperftools 仓库中随附的 gMock 源码与测试基础设施为佐证带你完整掌握 mock 的写法、期望的设定、次数约束、返回值行为与调用顺序控制最终在单元测试中实现快、稳、准的依赖隔离。gMock 是什么Mock 对象与 Fake 对象的本质区别当你编写原型或测试时往往不能也不应该完全依赖真实对象。mock 对象实现了与真实对象相同的接口因此可以被当作真实对象使用但允许你在运行时指定它应当如何被使用哪些方法会被调用、以什么顺序调用、调用多少次、传入什么参数、返回什么结果等等。在测试驱动开发TDD社区中fake与mock是两个经常被混淆但含义截然不同的概念Fake假对象拥有可工作的实现但通常走一些捷径例如让操作更廉价因此不适合用于生产环境。一个内存文件系统就是典型的 fake。Mock模拟对象预先用expectations期望编程的对象这些期望构成了它预期会收到哪些调用的规格说明。最核心的一点mock 允许你验证它自身与被测代码之间的交互。fake 与 mock 的差异在你真正开始使用 mock 后会愈发清晰。gMock正是这样一套用于创建并使用 mock 类的库官方文档戏称它为 framework 以显得更酷。它对 C 的意义相当于 jMock / EasyMock 对 Java 的意义。使用 gMock 的典型三步流程是用几个简单宏描述你要 mock 的接口宏会自动展开为 mock 类的实现创建若干 mock 对象用直观的语法设定其期望与行为运行使用这些 mock 对象的被测代码——一旦违反期望gMock 会立即捕获错误。在 gperftools 仓库中gMock 与 googletest 以 vendor 形式内置在 vendor/googletest 目录下主头文件 gmock.h 的注释明确说明了它所支持的ON_CALL与EXPECT_CALL语法形态可作为后续章节的语法权威参考。为什么需要 gMock手工 mock 的三大痛点虽然 mock 对象能帮助测试移除不必要的依赖、让测试又快又可靠但在 C 中手工编写 mock 极其痛苦必须有人实现 mock这项工作通常冗长且易错难怪开发者宁愿绕远路去避开它手工 mock 的质量难以预测——可能见到打磨精致的实现也可能见到赶工拼凑、带各种临时限制的版本从一个 mock 上学到的知识无法迁移到下一个 mock。反观 Java 和 Python 社区jMock、EasyMock 等成熟框架让 mock 的创建完全自动化mock 因此在那些社区里被证明是高效且被广泛采纳的技术——趁手的工具完全能改变结果。gMock 正是为 C 程序员打造的它受 jMock 与 EasyMock 启发但针对 C 的特性做了专门设计。如果你的项目遇到以下问题gMock 就是你的帮手受困于欠佳的设计希望早点多做原型验证但 C 的原型开发远谈不上快速测试依赖太多库或昂贵资源如数据库而运行缓慢测试依赖不可靠的资源如网络而脆弱易碎想测试代码对故障如文件校验和错误的处理却很难人为制造故障需要确认模块与其他模块的交互方式是否正确但交互难以观察只能事后观察副作用十分笨拙想mock 掉依赖但它们还没有 mock 实现而你又不信任那些手写的 mock。因此官方建议把 gMock 用作两用工具设计工具尽早、频繁地试验接口设计更多迭代带来更好设计与测试工具削减测试的外部依赖、探测模块与协作者之间的交互。快速起步gMock 随 googletest 一起分发gMock 与 googletest 捆绑发布。在 gperftools 仓库中二者的集成体现在构建系统层面根目录 CMakeLists.txt 将 vendor 下的 googletest 源码编译为静态库gtest包含gtest_main.cc、gtest.cc等源文件并对外暴露vendor/googletest/googletest/include头文件路径仓库内的大量单测如src/tests/addressmap_unittest.cc、src/tests/page_heap_test.cc等均以target_link_libraries(..., gtest)的方式链接该测试框架。在实际工程中使用时只需在你的构建中引入 googletest 源码并在测试代码中#include gmock/gmock.h即可开始。实战案例为海龟绘图接口编写 Mock假设你在开发一个依赖类 LOGO 语言 API 的绘图程序如何测试它画对了直接运行并与黄金截图比对是昂贵且脆弱的换一块抗锯齿更好的显卡就得更新全部黄金图片。正确做法是运用依赖注入不让应用程序直接调用系统 API而是把 API 包装进一个接口如Turtle并面向该接口编程class Turtle { ... virtual ~Turtle() {} virtual void PenUp() 0; virtual void PenDown() 0; virtual void Forward(int distance) 0; virtual void Turn(int degrees) 0; virtual void GoTo(int x, int y) 0; virtual int GetX() const 0; virtual int GetY() const 0; };注意Turtle的析构函数必须是虚函数——对所有打算被继承的类都是如此否则通过基类指针 delete 派生对象时不会调用派生类析构函数导致内存泄漏等程序状态损坏。PenUp()/PenDown()控制移动是否留下痕迹Forward()、Turn()、GoTo()控制移动GetX()/GetY()返回当前位置。程序在正常路径使用真实实现测试时则换成 mock 实现从而轻松检查程序调用了哪些绘图原语、以什么参数、按什么顺序调用。这样的测试更健壮不会因新机器抗锯齿方式不同而失败、更易读易维护测试意图写在代码里而非二进制图片中并且快得多。如何定义 Mock 类MOCK_METHOD 宏如果幸运你要用的 mock 可能已有人实现否则按以下步骤即可把写 mock 变成一场游戏从Turtle派生一个MockTurtle类挑一个Turtle的virtual函数用模板 mock 非虚方法也是可行的详见 Cook Book但要麻烦得多在子类的public:区段写下MOCK_METHOD();宏把函数签名剪切粘贴进宏中加上两个逗号——一个在返回类型与函数名之间另一个在函数名与参数列表之间若 mock 的是 const 方法加第 4 个参数(const)括号必须保留由于是在覆写虚方法建议加override关键字const 方法的第 4 个参数写作(const, override)非 const 方法写作(override)非强制重复直到所有想 mock 的虚函数完成抽象类中的所有纯虚方法必须被 mock 或覆写。完成后大致如下#include gmock/gmock.h // 引入 gMock class MockTurtle : public Turtle { public: ... MOCK_METHOD(void, PenUp, (), (override)); MOCK_METHOD(void, PenDown, (), (override)); MOCK_METHOD(void, Forward, (int distance), (override)); MOCK_METHOD(void, Turn, (int degrees), (override)); MOCK_METHOD(void, GoTo, (int x, int y), (override)); MOCK_METHOD(int, GetX, (), (const, override)); MOCK_METHOD(int, GetY, (), (const, override)); };无需在其他地方定义这些 mock 方法——MOCK_METHOD宏会替你生成全部定义。在仓库源码中该宏的定义位于 gmock-function-mocker.h它基于返回类型、方法名与参数列表展开出一整套 mock 桩代码gmock.h头文件顶部注释也给出了ON_CALL/EXPECT_CALL的完整语法骨架见 gmock.h。Mock 类放哪里所有权决定放置策略定义 mock 类时需决定放置位置。一些人放在_test.cc里当被 mock 的接口如Foo由同一个人或团队维护时没问题否则Foo一旦变更你的测试就可能编译失败——你无法指望Foo的维护者去修复每个使用Foo的测试。一般原则不要 mock 你不拥有的类。若必须 mock 他人所有的类则把 mock 类定义在Foo的 Bazel 包中通常是同目录或testing子目录放在.h头文件里并构建为testonlyTrue的cc_library供所有人从测试中引用。这样Foo变化时只需改一份MockFoo且只需修复依赖变更方法的测试。另一种做法是引入薄适配层FooAdaptor面向新接口编程。因为FooAdaptor归你所有吸收Foo的变化更容易虽然初期工作量更大但精心挑选的适配器接口可以让代码更好写、更可读长期来看是净收益。在测试中使用 Mock五步工作流拿到 mock 类后典型工作流如下从testing命名空间导入 gMock 名字以便不加限定地使用每个文件只需一次命名空间是好习惯创建若干 mock 对象设定期望方法将被调用多少次带什么参数应该做什么运行使用 mock 的代码可选地配合 googletest 断言检查结果。若 mock 方法被调用次数超过预期或参数错误会立即报错mock 析构时gMock 自动检查其上的全部期望是否已满足。示例#include path/to/mock-turtle.h #include gmock/gmock.h #include gtest/gtest.h using ::testing::AtLeast; // #1 TEST(PainterTest, CanDrawSomething) { MockTurtle turtle; // #2 EXPECT_CALL(turtle, PenDown()) // #3 .Times(AtLeast(1)); Painter painter(turtle); // #4 EXPECT_TRUE(painter.DrawCircle(0, 0, 10)); // #5 }该测试验证PenDown()至少被调用一次若painter没调用它测试会失败并输出类似信息path/to/my_test.cc:119: Failure Actual function call count doesnt match this expectation: Actually: never called; Expected: called at least once. Stack trace: ...Tip 1若在 Emacs 缓冲区内运行测试可在失败行号上按Enter直接跳转到失败的期望处。Tip 2如果 mock 对象从未被删除最终的期望校验就不会发生。因此当你在堆上分配 mock 时建议开启堆检查器若使用gtest_main库则自动获得该能力。期望必须在调用之前设定重要规则gMock 要求期望先于mock 函数被调用之前设定否则行为未定义。不要把EXPECT_CALL()与对 mock 函数的调用交替进行也不要在把 mock 传给某个 API 之后再对其设定期望。也就是说EXPECT_CALL()应被解读为期待未来会发生一次调用而非一次调用已经发生。这样设计的原因提前设定期望能让 gMock 在违规刚发生时上下文、堆栈等仍然可用立即上报极大方便调试。设定期望让交互规格精确到刚刚好成功使用 mock 的关键是设定恰到好处的期望太严则测试会因无关变更而失败太松则让 bug 溜走。gMock 提供了让你做到刚刚好的全部手段。一般语法EXPECT_CALL()宏用于在 mock 方法上设定期望通用语法为EXPECT_CALL(mock_object, method(matchers)) .Times(cardinality) .WillOnce(action) .WillRepeatedly(action);宏有两个参数先是 mock 对象然后是方法及其参数。注意二者之间用**逗号,**而非句点.分隔这是出于技术原因的必要设计。若方法未被重载宏也可以不带 matcher 调用EXPECT_CALL(mock_object, non-overloaded-method) .Times(cardinality) .WillOnce(action) .WillRepeatedly(action);这种形式允许测试作者直接表达以任意参数调用而不必写出参数个数或类型为避免歧义它仅可用于非重载方法。两种形式后都可以接若干可选clauses子句下文逐一介绍。该语法刻意让期望读起来像英文。例如using ::testing::Return; ... EXPECT_CALL(turtle, GetX()) .Times(5) .WillOnce(Return(100)) .WillOnce(Return(150)) .WillRepeatedly(Return(200));意思是turtle.GetX()将被调用 5 次第 1 次返回 100第 2 次返回 150之后每次都返回 200。有人把这种风格称为领域特定语言DSL。为什么用宏一是让期望易于识别无论是用grep还是人眼二是让 gMock 能在失败信息中包含期望所在的源文件位置方便调试。这正是EXPECT_CALL宏被设计出来的双重目的在 gmock-spec-builders.h 等实现文件中可以看到它围绕期望expectation与匹配matching的完整状态机。Matcher我们期待什么参数当 mock 函数带参数时可以指定期望的参数值// 期待乌龟向前移动 100 个单位。 EXPECT_CALL(turtle, Forward(100));但经常你不想太具体——过度指定会导致测试脆弱并掩盖意图。官方建议只指定必要的内容。如果不在意某个参数的值写_表示任意值using ::testing::_; ... // 期待乌龟跳到 x50 这条线上的某处。 EXPECT_CALL(turtle, GoTo(50, _));_就是所谓的matcher匹配器的一个实例。matcher 类似谓词能测试参数是否符合期望_是任意值的便捷写法。上例中的100、50其实也是 matcher——隐式等价于Eq(100)、Eq(50)即参数必须用operator等于给定值。gMock 为常见类型内置了大量 matcher也支持自定义 matcher例如using ::testing::Ge; ... // 期待乌龟至少向前移动 100。 EXPECT_CALL(turtle, Forward(Ge(100)));如果你对所有参数都不在意与其逐个写_不如直接省略参数列表// 期待乌龟向前移动。 EXPECT_CALL(turtle, Forward); // 期待乌龟跳到某处。 EXPECT_CALL(turtle, GoTo);这适用于所有非重载方法若方法被重载则需要通过指定参数个数乃至参数类型来帮助 gMock 解析期望的是哪个重载。Cardinality将被调用多少次Times()是EXPECT_CALL()后可以指定的第一个子句其参数称为cardinality基数表示调用应发生的次数。它允许你重复一条期望而无需写很多遍更重要的是cardinality 可以像 matcher 一样是模糊的从而精确表达测试意图。一个有趣的特殊情况是Times(0)表示该函数根本不应被以给定参数调用一旦被错误地调用gMock 就会上报 googletest 失败。上文已见过AtLeast(n)这种模糊基数内置基数的完整清单见 gmock_cheat_sheet.md。在源码层面基数类型定义于 gmock-cardinalities.h。Times()子句可以省略。省略时 gMock 会自行推断基数规则很好记若EXPECT_CALL()中既无WillOnce()也无WillRepeatedly()推断的基数为Times(1)若有n个WillOnce()但没有WillRepeatedly()n≥ 1基数为Times(n)若有n个WillOnce()且有一个WillRepeatedly()n≥ 0基数为Times(AtLeast(n))。小测验如果一个函数被期望调用两次、实际却被调用四次会发生什么Action它应该做什么mock 对象并没有真正的工作实现需要用户告诉它在方法被调用时做什么。首先若 mock 函数的返回类型是内建类型或指针它自带默认动作void函数直接返回bool函数返回false其他函数返回 0在 C11 及以上返回类型可默认构造的 mock 函数默认返回默认构造的值。如果你什么都不说就用这个行为。其次若函数没有默认动作、或默认动作不合需求可以用一串WillOnce()子句加一个可选的WillRepeatedly()来指定每次期望匹配时要执行的动作。例如using ::testing::Return; ... EXPECT_CALL(turtle, GetX()) .WillOnce(Return(100)) .WillOnce(Return(200)) .WillOnce(Return(300));表示turtle.GetX()将被调用恰好三次因未显式写Times()gMock 从WillOnce()的数量推断依次返回 100、200、300。using ::testing::Return; ... EXPECT_CALL(turtle, GetY()) .WillOnce(Return(100)) .WillOnce(Return(200)) .WillRepeatedly(Return(300));表示turtle.GetY()将被调用至少两次前两次分别返回 100、200从第三次起返回 300。当然若显式写了Times()gMock 不会自行推断。若指定的次数多于WillOnce()子句的个数则所有WillOnce()用尽之后gMock 每次都执行默认动作除非还有WillRepeatedly()。除了Return()WillOnce()里还能做什么可以用ReturnRef(variable)返回引用或调用预定义函数等详见 gmock_cook_book.md。重要注意EXPECT_CALL()语句只对 action 子句求值一次即使该 action 会被执行多次。因此必须当心副作用——下面的代码很可能不符合预期using ::testing::Return; ... int n 100; EXPECT_CALL(turtle, GetX()) .Times(4) .WillRepeatedly(Return(n));它不会依次返回 100、101、102……而是每次都返回 100因为n只被求值了一次。同理Return(new Foo)会在EXPECT_CALL()执行时创建一次Foo对象之后每次都返回同一个指针。若希望副作用每次发生需要定义自定义 action见 Cook Book。再来一次测验猜猜下面代码的含义using ::testing::Return; ... EXPECT_CALL(turtle, GetY()) .Times(4) .WillOnce(Return(100));显然turtle.GetY()被期望调用四次。但如果你以为每次都返回 100那就错了每调用一次函数就会消耗一个WillOnce()子句之后执行默认动作。正确答案是第一次返回 100从第二次起返回 0——返回 0 正是int函数的默认动作。多条期望并存反向查找与新规则覆盖旧规则实际测试中通常会在多个 mock 方法可能来自多个 mock 对象上设定多条期望。默认情况下当 mock 方法被调用时gMock 会按定义的反向顺序搜索期望在找到一条与参数匹配的活跃期望时停止可理解为新规则覆盖旧规则。如果匹配的期望已不能再接受调用就会得到上界违规upper-bound-violated失败。示例using ::testing::_; ... EXPECT_CALL(turtle, Forward(_)); // #1 EXPECT_CALL(turtle, Forward(10)) // #2 .Times(2);若Forward(10)连续被调用三次第三次就会报错因为最后匹配的期望 #2 已饱和但如果第三次调用的是Forward(20)则没问题——此时 #1 成为匹配期望。为什么反向搜索这允许用户在 mock 对象的构造函数或测试夹具的 setup 阶段设置默认期望然后在测试体中写更具体的期望来定制 mock。因此如果同一方法有两条期望应把 matcher 更具体的那条放在后面否则更具体的规则会被后面更宽泛的规则遮蔽。Tip常见做法是先为某个方法写一条兜底期望配Times(AnyNumber())省略参数若重载则对全部参数用_。这让对该方法的任何调用都被视为预期。对于完全没有提及的方法无趣调用不是必需的但对于既有部分期望、其他调用也无所谓的方法非常有用。更深入的概念见 Understanding Uninteresting vs Unexpected Calls。有序与无序调用InSequence 控制严格顺序默认情况下即使前面的期望尚未满足一条期望也可以匹配调用——即调用不必按期望指定的顺序发生。若希望所有期望调用严格按顺序发生gMock 提供InSequenceusing ::testing::InSequence; ... TEST(FooTest, DrawsLineSegment) { ... { InSequence seq; EXPECT_CALL(turtle, PenDown()); EXPECT_CALL(turtle, Forward(100)); EXPECT_CALL(turtle, PenUp()); } Foo(); }创建一个InSequence类型对象后其作用域内的所有期望都被放入一条sequence必须依次发生。由于实际工作由该对象的构造与析构完成它的名字无关紧要。本例验证Foo()按书写顺序调用这三个函数乱序即报错。如果你只关心部分调用的相对顺序任意偏序答案也是肯定的——细节见 gmock_cook_book.md。期望默认是粘性的RetiresOnSaturation来个小测验如何测试乌龟被要求回到原点恰好两次忽略它收到的其他指令参考答案using ::testing::_; using ::testing::AnyNumber; ... EXPECT_CALL(turtle, GoTo(_, _)) // #1 .Times(AnyNumber()); EXPECT_CALL(turtle, GoTo(0, 0)) // #2 .Times(2);假设turtle.GoTo(0, 0)被调用三次第三次时 gMock 发现参数匹配期望 #2记住总是选择最后匹配的期望而该期望只允许两次于是立即报错。这就是上节多条期望规则的直接应用。这个例子揭示了重要规则gMock 中的期望默认是粘性sticky的——即使已经达到调用上界它们仍然保持活跃。这与许多其他 mock 框架的行为不同之所以这样设计是因为 gMock 认为该规则让常见情况更容易表达和理解。再看你是否真正理解了下面代码表达什么using ::testing::Return; ... for (int i n; i 0; i--) { EXPECT_CALL(turtle, GetX()) .WillOnce(Return(10*i)); }如果你以为turtle.GetX()将被调用n次并依次返回 10、20、30……那就错了因为期望是粘性的第二次调用turtle.GetX()时最后最新那条EXPECT_CALL()就会匹配并立刻触发上界违规错误——这段代码几乎没用。正确的做法是显式声明期望不粘即饱和后立即retire退役using ::testing::Return; ... for (int i n; i 0; i--) { EXPECT_CALL(turtle, GetX()) .WillOnce(Return(10*i)) .RetiresOnSaturation(); }还有更好的做法既然这里期望按特定顺序发生、action 也按顺序排列应该用 sequence 显式表达顺序using ::testing::InSequence; using ::testing::Return; ... { InSequence s; for (int i 1; i n; i) { EXPECT_CALL(turtle, GetX()) .WillOnce(Return(10*i)) .RetiresOnSaturation(); } }顺便一提期望可能不粘的另一种情形是它处于某个 sequence 中——一旦 sequence 中排在其后的期望被使用它便自动退役从此不再匹配任何调用。无趣调用Uninteresting Calls不想管的就别管mock 对象可能有很多方法并非都值得关心——例如某些测试中并不在意GetX()/GetY()被调用了多少次。在 gMock 中如果对某个方法不感兴趣什么都不说即可。若该方法被调用测试输出中会出现一条警告但不会导致失败。这被称为 naggy唠叨行为如需改变可参考 The Nice, the Strict, and the Naggy。在源码中NiceMock、StrictMock、NaggyMock等混合类mixin定义于 gmock-nice-strict.h分别对应无趣调用不警告 / 无趣调用即失败 / 无趣调用发警告三种默认策略测试时可据此灵活切换。小结gMock 心法速记mock 对象 同一接口 运行时指定的调用期望用来验证交互而非实现用MOCK_METHOD一行声明一个 mock 方法宏自动生成实现只 mock 你拥有的接口或通过适配层隔离变化EXPECT_CALL必须在调用发生前设定matcher 控制参数_表示任意、Times()控制次数可省略并由 gMock 推断、WillOnce()/WillRepeatedly()控制返回值与副作用多条期望反向匹配、新规则覆盖旧规则因此更具体的期望要写在后面期望默认粘性需要时用RetiresOnSaturation()退役InSequence强制严格调用顺序不关心的调用保持沉默即可naggy 行为或用 NiceMock / StrictMock 调整策略。掌握以上要点后你就能像 gperftools 的测试套件见 src/tests 下大量基于 gtest 的单测一样用 googletest gMock 这套组合写出既快又稳、交互意图清晰可读的 C 单元测试。赞分享性能剖析内存管理开发工具【免费下载链接】gperftoolsMain gperftools repository项目地址https://gitcode.com/gh_mirrors/gp/gperftools点击查看免费下载相关推荐miniblink49 中的 Google Mock 入门用 C 模拟对象写出可验证交互的单元测试miniblink49 中的 Google Mock 入门用 C 模拟对象写出可验证交互的单元测试 导读 本篇文章以 miniblink49 仓库内置的前端桌面应用深入理解 gMock for DummiesC 单元测试中的 Mock 对象、期望设置与交互验证实战指南深入理解 gMock for DummiesC 单元测试中的 Mock 对象、期望设置与交互验证实战指南 导读 gMockGoogle Mock是随人工智能大模型推理引擎本地部署miniblink49 内嵌 Google Mock 实战指南用 C 模拟对象写出可靠、可交互验证的单元测试miniblink49 内嵌 Google Mock 实战指南用 C 模拟对象写出可靠、可交互验证的单元测试 Google MockGoogle C前端桌面应用上一篇终极指南Inpaint-Anything如何通过AI图像修复技术革新医疗影像分析下一篇C HTTP请求终极指南5种HttpVersion协议版本控制策略详解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表