- 移动开发
- 跨平台
- 原生移动
- 前端
【免费下载链接】incubator-weex
Apache Weex (Incubating)
导读
本文基于 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));设计文档指出这种方式存在三个明显的不足:
- 冗余的占位参数:即便该 Action 只关心 mock 函数的第二个参数,定义时仍必须把其余参数全部列出(用
Unused占位),非常繁琐; - 参数个数被锁死:这样定义的 Action 只能用在"恰好有 3 个参数"的 mock 函数上,复用性差;
- 调用语法不自然:使用者必须写
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_type | mock 函数第 K 个(0 起始)参数的类型 |
args | mock 函数的全部参数,以元组(tuple)形式呈现 |
args_type | mock 函数全部参数组成的元组类型 |
return_type | mock 函数的返回类型 |
function_type | mock 函数的类型 |
设计文档以一个桩 Action 示例int DoSomething(bool flag, int* ptr);给出了完整的符号绑定对照表:
| 预定义符号 | 绑定值/类型 |
|---|---|
arg0 | flag的值 |
arg0_type | bool类型 |
arg1 | ptr的值 |
arg1_type | int*类型 |
args | 元组(flag, ptr) |
args_type | std::tr1::tuple<bool, int*>类型 |
return_type | int类型 |
function_type | int(bool, int*)类型 |
注意:args_type中出现的tr1::tuple是文档写作时的 C++ 标准库形态(std::tr1);当前仓库实现已演进为 C++11 风格,见下文"源码实现印证"。
三、参数化 Action:ACTION_P与ACTION_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_P2、ACTION_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宏分别生成不同后缀的类(PlusActionP与PlusActionP2),因此互不冲突。
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,请同时考虑其他实现手段(如ActionInterface或MakePolymorphicAction());- 其他方式虽然工作量大,但能对 mock 函数实参与 Action 参数的类型施加更细粒度的控制,通常能产生更好的编译器报错信息,长期看收益更大;
- 它们还允许基于参数类型重载 Action,而不仅仅基于参数个数。
简言之:宏换来了简洁,也让出了类型控制力——这是一条明确的设计权衡。
六、相关工作:为什么不用 Boost Lambda 库
设计文档敏锐地指出,ACTION*宏实质上是在模仿闭包(lambda 表达式 / 匿名函数),两者目标都是降低定义函数时的语法开销。C++0x 将原生支持 lambda,但文档写作时 lambda 尚未进入 C++ 标准;当时一些非标准库(最典型的是BLL,即 Boost Lambda Library)试图缓解此问题,但设计文档认为它们不适合用来定义 Action,理由如下:
- 非标准且安装不普遍:Google Mock 只依赖标准库与
tr1::tuple(属于新 C++ 标准、gcc 4+ 自带),希望保持这一纯净依赖; - 学习成本不低:BLL 并不易学;
- 会被 C++0x lambda 淘汰:不愿让用户依赖一个行将消亡的库;
- 基于操作符、过于临时:无法书写语句块,也无法把 lambda 参数传给函数;
- 语义微妙、易迷惑新手:例如表达式
_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_P、MATCHER_P2……等参数化版本。
九、源码实现印证:从设计提案到落地宏
设计文档是"提案",而当前仓库保留了该设计的最终实现,可直接对照阅读,验证每一条设计决策的落地形态。
9.1 ACTION 宏族的实现
在 gmock-generated-actions.h 中,可以依次找到:
- ACTION(name):生成
name##Action类 + 内联工厂函数name()。内部定义嵌套类gmock_Impl<F>,它继承::testing::ActionInterface<F>,并提供function_type、return_type、args_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_P2、ACTION_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)>,实现MatchAndExplain、DescribeTo、DescribeNegationTo三个虚函数; - 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 完整走过了从"问题定义 → 方案设计 → 细节规范 → 权衡说明 → 演进规划"的设计闭环:
- 问题:C++ 缺乏闭包,自定义 Action 样板代码过多,
Invoke方案限制参数个数、语法不自然; - 方案:
ACTION*宏族以最小样板定义 Action,argK/args/return_type等预定义符号与编译器类型推断保证简洁且类型安全; - 扩展:
ACTION_P~ACTION_P9支持参数化,按参数个数重载、param_type/StaticAssertTypeEq类型约束、统一的对象类型命名规则; - 权衡:宏简单但放弃类型控制,重用途场景建议改用
ActionInterface/MakePolymorphicAction(); - 对照:相对 BLL 等非标准库的五大优势,以及基于 C++0x 演进(lambda、函数局部类型、组合)的未来规划;
- 落地:上述设计最终在 gmock-generated-actions.h 与 gmock-generated-matchers.h 中原样实现(含
MATCHER宏额外引入description参数这一实现期优化),并为 Weex 的 C++ 核心测试所携带。
对于任何希望深入 C++ 测试框架内部、理解"如何用宏在无闭包语言中优雅模拟闭包"的开发者,这份文档与仓库内实现构成了一个完整的、可对照研读的案例。
- 移动开发
- 跨平台
- 原生移动
- 前端
【免费下载链接】incubator-weex
Apache Weex (Incubating)
相关推荐
基于 v8 内嵌 Google Mock 的 ACTION/MATCHER 自定义宏设计:从 DesignDoc 到源码实现的全解
基于 v8 内嵌 Google Mock 的 ACTION/MATCHER 自定义宏设计:从 DesignDoc 到源码实现的全解 本文以 V8 引擎测试栈中内
前端桌面应用MiniBlink49 内置 Google Mock ACTION* 自定义动作宏设计解析:v8_6_7 测试栈中的 Action/Matcher 宏机制
MiniBlink49 内置 Google Mock ACTION 自定义动作宏设计解析:v8_6_7 测试栈中的 Action/Matcher 宏机制 本文以
前端桌面应用miniblink49 中 v8_5_7 测试栈的 Google Mock 设计文档深度解读:ACTION/MATCHER 宏如何在 C++03 里造出"lambda"
miniblink49 中 v8_5_7 测试栈的 Google Mock 设计文档深度解读:ACTION/MATCHER 宏如何在 C++03 里造出"lam
前端桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考