Apache Weex 内嵌 Google Mock 设计文档解读:ACTION* 与 MATCHER* 宏的架构设计与实践
2026/9/22 19:27:25 网站建设 项目流程
  • 移动开发
  • 跨平台
  • 原生移动
  • 前端

【免费下载链接】incubator-weex

Apache Weex (Incubating)

项目地址:https://gitcode.com/gh_mirrors/in/incubator-weex
点击查看免费下载

导读

本文基于 Apache Weex(Incubating)仓库中随附的 Google Mock 设计文档 DesignDoc.md,系统讲解 Google Mock 用于"零样板自定义 Action(桩行为)与 Matcher(参数匹配器)"的宏体系设计:从ACTION(name)ACTION_P(name, param)ACTION_P9,以及对应的MATCHER*宏族。读者将理解这些宏要解决的 C++ 测试痛点、其预定义符号与类型推断机制、参数化与重载规则、类型约束技巧,以及它们在当前仓库 googlemock 中从设计提案到 gmock-generated-actions.h 与 gmock-generated-matchers.h 落地实现的全过程。

Google Mock 是 Google 的 C++ 测试框架 README.md 明确说明的 C++ 单元测试配套方案,被 Weex 的 C++ 核心(weex_core)测试体系随 googletest 一并引入,用于为跨平台原生代码编写 mock 与断言。


一、设计动机:C++ 缺乏闭包导致的 Action 定义之痛

设计文档开篇就点明核心问题:由于 C++ 标准(当时)缺乏闭包(closure)机制,在 Google Mock 中定义一个自定义 Action 需要付出不小的代价

假设你想实现"递增 mock 函数第二个参数所指向的值,并返回它",传统方式需要先写一个普通函数,再用Invoke包装:

int IncrementArg1(Unused, int* p, Unused) { return ++(*p); } ... WillOnce(Invoke(IncrementArg1));

设计文档指出这种方式存在三个明显的不足:

  1. 冗余的占位参数:即便该 Action 只关心 mock 函数的第二个参数,定义时仍必须把其余参数全部列出(用Unused占位),非常繁琐;
  2. 参数个数被锁死:这样定义的 Action 只能用在"恰好有 3 个参数"的 mock 函数上,复用性差;
  3. 调用语法不自然:使用者必须写Invoke(IncrementArg1),而不是更直观的IncrementArg1()

1.1MakePolymorphicAction()的救赎与代价

文档进一步说明,后两个问题可以通过MakePolymorphicAction()解决,但代价是大量样板代码:

class IncrementArg1Action { public: template <typename Result, typename ArgumentTuple> Result Perform(const ArgumentTuple& args) const { return ++(*tr1::get<1>(args)); } }; PolymorphicAction<IncrementArg1Action> IncrementArg1() { return MakePolymorphicAction(IncrementArg1Action()); } ... WillOnce(IncrementArg1());

可以看到,虽然现在可以像调用函数一样使用IncrementArg1(),但使用者被迫手写一个模板类、实现Perform成员函数并手工解析参数元组(tr1::get<1>(args))。设计文档给出的目标是:

让用户以 C++ 所能允许的最小样板代码量来定义自定义 Action。


二、核心方案:ACTION(name)

设计文档提出引入一个新宏来消灭样板代码:

ACTION(name) { statements; }

在命名空间作用域(namespace scope)中使用它,就会定义一个名为name的 Action。宏体内部可以引用 mock 函数的第 K 个(0 起始)参数,符号名为argK。前述"递增第二个参数并返回"的例子可以改写为:

ACTION(IncrementArg1) { return ++(*arg1); }

之后即可直接使用:

... WillOnce(IncrementArg1());

2.1 简洁是设计目标,类型安全是底线

设计文档强调,使用ACTION宏时不需要指定 mock 函数参数的类型——简洁是首要设计目标。但这并不意味着失去类型安全:

  • 如果*arg1不支持++运算符,编译器会报错;
  • 如果++(*arg1)的类型与 mock 函数返回类型不兼容,编译器同样会报错。

即"类型由编译器推断",类型错误在编译期暴露,而非运行期。

一个更综合的示例(展示宏体内多语句能力):

ACTION(Foo) { (*arg2)(5); // 以 5 调用第 2 个参数(函数指针) Blah(); // 调用普通函数 Blah() *arg1 = 0; // 把第 1 个参数指向的值设为 0 return arg0; // 返回第 0 个参数 }

2.2 预定义符号表

为方便与灵活,ACTION宏体内部还提供了以下预定义符号(设计文档原文表格):

符号含义
argK_typemock 函数第 K 个(0 起始)参数的类型
argsmock 函数的全部参数,以元组(tuple)形式呈现
args_typemock 函数全部参数组成的元组类型
return_typemock 函数的返回类型
function_typemock 函数的类型

设计文档以一个桩 Action 示例int DoSomething(bool flag, int* ptr);给出了完整的符号绑定对照表:

预定义符号绑定值/类型
arg0flag的值
arg0_typebool类型
arg1ptr的值
arg1_typeint*类型
args元组(flag, ptr)
args_typestd::tr1::tuple<bool, int*>类型
return_typeint类型
function_typeint(bool, int*)类型

注意:args_type中出现的tr1::tuple是文档写作时的 C++ 标准库形态(std::tr1);当前仓库实现已演进为 C++11 风格,见下文"源码实现印证"。


三、参数化 Action:ACTION_PACTION_P2~ACTION_P9

很多时候 Action 需要携带参数。设计文档提出第二个宏:

ACTION_P(name, param) { statements; }

例如:

ACTION_P(Add, n) { return arg0 + n; }

即可写出:

// 返回参数 #0 + 5。 ... WillOnce(Add(5));

3.1 两个容易混淆的术语

设计文档在此处特意定义了术语,避免混淆:

  • arguments(实参):调用 mock 函数时传入的值;
  • parameters(参数):实例化(构造)某个 Action 时传入的值。

Add中的n是参数(parameter),arg0则来自 mock 函数的实参(argument)。

ACTION一样,ACTION_P也无需声明参数类型——编译器会自动推断。若参数名为param,还可以用 Google Mock 预定义的符号param_type引用其被推断出的类型。

3.2 多参数版本

为支持多参数 Action,设计文档明确将提供ACTION_P2ACTION_P3……以此类推。示例:

ACTION_P2(ReturnDistanceTo, x, y) { double dx = arg0 - x; double dy = arg1 - y; return sqrt(dx*dx + dy*dy); }

使用:

... WillOnce(ReturnDistanceTo(5.0, 26.5));

文档还给出一个重要视角:ACTION可以看作参数个数为 0 的参数化 Action 的退化形式——这为下面统一的类型命名规则埋下伏笔。


四、高级用法

4.1 按参数个数重载 Action

可以轻松定义按参数个数重载的 Action:

ACTION_P(Plus, a) { ... } ACTION_P2(Plus, a, b) { ... }

两个Plus宏分别生成不同后缀的类(PlusActionPPlusActionP2),因此互不冲突。

4.2 限制参数或实参的类型

为了最大限度的简洁与可复用,ACTION*宏族刻意不允许直接声明 mock 函数实参与 Action 参数的类型,而把类型推断交给编译器。若确实需要显式约束类型,设计文档给出几种技巧:

ACTION(Foo) { // 强制 arg0 可转换为 int int n = arg0; ... use n instead of arg0 here ... } ACTION_P(Bar, param) { // 强制 arg1 的类型为 const char* ::testing::StaticAssertTypeEq<const char*, arg1_type>(); // 强制 param 可转换为 bool bool flag = param; }

其中StaticAssertTypeEq是计划加入 Google Test 的编译期断言(命名与 C++0x 的static_assert对齐)。

4.3 Action 对象类型命名规则

若你写的函数需要返回一个ACTION对象,就必须知道它的类型。设计文档给出了简洁的命名规则(与参数个数一一对应):

定义形式表达式对象类型
ACTION(Foo)Foo()FooAction
ACTION_P(Bar, param)Bar(int_value)BarActionP<int>
ACTION_P2(Baz, p1, p2)Baz(bool_value, int_value)BazActionP2<bool, int>
.........

后缀规律:Action(0 参)、ActionP(1 参)、ActionP2(2 参)……必须为不同参数个数的 Action 挑选不同后缀,否则无法按参数个数实现重载。


五、什么时候该用,什么时候不该用

设计文档专门给出了"使用建议"章节,提醒用户宏并非万能:

  • ACTION*宏非常方便,但如果你需要大量复用某个 Action,请同时考虑其他实现手段(如ActionInterfaceMakePolymorphicAction());
  • 其他方式虽然工作量大,但能对 mock 函数实参与 Action 参数的类型施加更细粒度的控制,通常能产生更好的编译器报错信息,长期看收益更大;
  • 它们还允许基于参数类型重载 Action,而不仅仅基于参数个数。

简言之:宏换来了简洁,也让出了类型控制力——这是一条明确的设计权衡。


六、相关工作:为什么不用 Boost Lambda 库

设计文档敏锐地指出,ACTION*宏实质上是在模仿闭包(lambda 表达式 / 匿名函数),两者目标都是降低定义函数时的语法开销。C++0x 将原生支持 lambda,但文档写作时 lambda 尚未进入 C++ 标准;当时一些非标准库(最典型的是BLL,即 Boost Lambda Library)试图缓解此问题,但设计文档认为它们不适合用来定义 Action,理由如下:

  1. 非标准且安装不普遍:Google Mock 只依赖标准库与tr1::tuple(属于新 C++ 标准、gcc 4+ 自带),希望保持这一纯净依赖;
  2. 学习成本不低:BLL 并不易学;
  3. 会被 C++0x lambda 淘汰:不愿让用户依赖一个行将消亡的库;
  4. 基于操作符、过于临时:无法书写语句块,也无法把 lambda 参数传给函数;
  5. 语义微妙、易迷惑新手:例如表达式_1++ + foo++中,foo在表达式求值时只自增一次,而_1在每次调用匿名函数时都会自增——远非直观。

ACTION*宏族则完全规避了上述所有问题。


七、未来改进方向

设计文档在文末记录了三条演进设想,体现了作者对 C++ 标准演进的预判:

  • 组合ACTION*:即在一个ACTION*内部调用另一个ACTION。作者并不确定是否需要,因为把ACTION定义放进函数模板再组合函数模板可达到类似效果,将根据用户反馈再定;
  • 支持在函数体内使用ACTION*():当时 C++ 标准不允许用函数局部类型(function-local types)实例化模板,因此ACTION*()只能出现在命名空间作用域。C++0x 将解除此限制,届时可重新审视实现;
  • 支持 lambda 作为 Action:C++0x 引入 lambda 后,可能会支持直接以 lambda 充当 Action。

仓库实现印证:在 gmock-generated-matchers.h 的文件头注释中,仍保留着"MATCHER*()只能用于命名空间作用域,原因是 C++ 尚不允许用函数局部类型实例化模板;C++0x 将修复此问题,届时再考虑支持在函数内使用"的说明——设计文档的这条限制原样延续到了实现代码里。


八、姊妹方案:MATCHER*宏的设计

设计文档在 Action 宏之后,规划了同样思路的匹配器宏:

MATCHER(name) { statements; }

宏体内可用arg引用被匹配的值。例如:

MATCHER(IsPositive) { return arg > 0; }

之后IsPositive()即成为一个匹配器:当且仅当被匹配值大于 0 时匹配成功。设计文档同时规划了MATCHER_PMATCHER_P2……等参数化版本。


九、源码实现印证:从设计提案到落地宏

设计文档是"提案",而当前仓库保留了该设计的最终实现,可直接对照阅读,验证每一条设计决策的落地形态。

9.1 ACTION 宏族的实现

在 gmock-generated-actions.h 中,可以依次找到:

  • ACTION(name):生成name##Action类 + 内联工厂函数name()。内部定义嵌套类gmock_Impl<F>,它继承::testing::ActionInterface<F>,并提供function_typereturn_typeargs_type三个 typedef——正是设计文档第 2.2 节符号表的实现来源;同时声明了gmock_PerformImpl,参数列表从arg0_type一直到arg9_type(支持最多 10 个实参);
  • ACTION_P(name, p0):生成模板类name##ActionP<p0_type>,用p0_type承接被推断的参数类型,类中保存成员p0,并提供operator ::testing::Action<F>()隐式转换;
  • ACTION_P2(name, p0, p1) 及 ACTION_P3、ACTION_P4、ACTION_P5、ACTION_P6、ACTION_P7、ACTION_P8、ACTION_P9:逐级增加模板参数,直到ACTION_P9(9 个参数),与设计文档"提供ACTION_P2ACTION_P3等"的规划完全一致。

关键印证点:宏生成的类名后缀Action/ActionP/ActionP2……与设计文档第 4.3 节的命名规则表格逐字吻合,可见实现严格遵循了设计提案。

9.2 MATCHER 宏族的实现

在 gmock-generated-matchers.h 中:

  • MATCHER(name, description):生成name##Matcher类,嵌套gmock_Impl<arg_type>继承::testing::MatcherInterface<GTEST_REFERENCE_TO_CONST_(arg_type)>,实现MatchAndExplainDescribeToDescribeNegationTo三个虚函数;
  • MATCHER_P(name, p0, description):模板类name##MatcherP<p0_type>
  • 依次提供 MATCHER_P2 直到 MATCHER_P10(最多 10 个参数)。

与设计文档的一处可见差异:最终实现中每个 MATCHER 宏都额外带有一个description参数——在宏体返回空描述时,实现会用FormatMatcherDescription根据名称与参数自动生成人类可读的匹配器描述,用于失败信息输出。这正是"设计提案在落地过程中被完善"的典型例证,也从侧面说明DescribeTo/DescribeNegationTo需要稳定可靠的文本来源。

9.3 在 Weex 仓库中的定位

当前仓库 googlemock 位于weex_core/test/third_party/googletest/下,与 googletest 本体同属weex_coreC++ 测试基础设施的第三方依赖。在 googlemock/README.md 中,Google Mock 的能力被概括为:用简单宏轻松创建 mock 类、提供丰富的 matcher 与 action、支持无序/部分有序/完全有序的期望约束、且可由用户自行扩展——其中"用宏轻松创建 mock"与"由用户扩展新 matcher 与 action"两点,正是本文所述ACTION*/MATCHER*宏设计所提供的核心能力。该 README 还建议新用户按"Google Test 基础 → ForDummies → 构建说明"的顺序入门,而 DesignDoc.md 则面向希望理解宏机制内部设计的开发者。


十、总结:一份设计文档的完整生命周期

回顾全文,DesignDoc.md 完整走过了从"问题定义 → 方案设计 → 细节规范 → 权衡说明 → 演进规划"的设计闭环:

  1. 问题:C++ 缺乏闭包,自定义 Action 样板代码过多,Invoke方案限制参数个数、语法不自然;
  2. 方案ACTION*宏族以最小样板定义 Action,argK/args/return_type等预定义符号与编译器类型推断保证简洁且类型安全;
  3. 扩展ACTION_P~ACTION_P9支持参数化,按参数个数重载、param_type/StaticAssertTypeEq类型约束、统一的对象类型命名规则;
  4. 权衡:宏简单但放弃类型控制,重用途场景建议改用ActionInterface/MakePolymorphicAction()
  5. 对照:相对 BLL 等非标准库的五大优势,以及基于 C++0x 演进(lambda、函数局部类型、组合)的未来规划;
  6. 落地:上述设计最终在 gmock-generated-actions.h 与 gmock-generated-matchers.h 中原样实现(含MATCHER宏额外引入description参数这一实现期优化),并为 Weex 的 C++ 核心测试所携带。

对于任何希望深入 C++ 测试框架内部、理解"如何用宏在无闭包语言中优雅模拟闭包"的开发者,这份文档与仓库内实现构成了一个完整的、可对照研读的案例。

  • 移动开发
  • 跨平台
  • 原生移动
  • 前端

【免费下载链接】incubator-weex

Apache Weex (Incubating)

项目地址:https://gitcode.com/gh_mirrors/in/incubator-weex
点击查看免费下载

相关推荐

上一篇:三分钟搞定B站缓存视频:m4s转MP4的傻瓜式完整教程
下一篇:fumadocs-python 指南:为 Fumadocs 一键生成 Python API 文档(运行时内容源与构建期 MDX 转换)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询