Mockery 与 PHPUnit 原生 Mock 差异
解读
在国内一线/二线互联网公司的 PHP 面试中,单元测试能力已从“加分项”变成“硬性门槛”。
面试官抛出“Mockery 与 PHPUnit 原生 Mock 差异”时,通常想验证三件事:
- 你是否真的写过测试(能说出痛点);
- 你是否理解“测试替身”的底层原理(隔离、验证、桩件);
- 你是否能在团队里做技术选型(性能、可读性、维护成本)。
回答时切忌只罗列 API,而要结合业务场景、运行环境、CI 成本给出“什么时候用谁”的明确结论。
知识点
- 生成机制
PHPUnit 原生 mock 使用继承+代码生成(eval)在内存中动态创建类;Mockery 同样代码生成,但代理层多包了一层“Expectation”对象,支持链式 API。 - 语法风格
PHPUnit 采用“构建器”模式,方法桩与预期分开声明;Mockery 提供类 jQuery 的连贯接口,可以在一句代码里完成“桩+预期+返回值”。 - 验证粒度
PHPUnit 只能验证“调用次数”与“参数等于”;Mockery 内置“hamcrest”匹配器,可验证“数组子集”“对象属性”“回调闭包”,对 Legacy 代码更友好。 - 性能差异
在 PHP 7.4 + OPcache 环境下,PHPUnit 原生 mock 由于少一层代理,单测跑 1000 个用例约快 15 %~20 %;Mockery 的 Expectation 对象在复杂约束时会多 1 次反射调用。 - 集成成本
PHPUnit 9 以后自带registerMockObject(),可与--strict-mark-output无缝整合;Mockery 需要额外Mockery::close()放在tearDown(),否则会出现“未满足期望”的延迟报错,CI 日志容易误判。 - 国内框架生态
Laravel 默认自带Mockery($this->mock()助手函数),Hyperf、ThinkPHP 8 的单元测试骨架也是phpunit.xml+Mockery组合;Symfony 官方文档示例仍以 PHPUnit 原生 mock 为主。 - 静态分析友好度
PHPUnit 生成的 mock 类带@phpstan-type标注,PhpStorm 可自动提示;Mockery 返回的是混合类型,需要额外写@var断言,否则 Stan 级别 6 以上会报“未找到方法”。 - 致命缺陷场景
对final/private方法,PHPUnit 原生直接报错;Mockery 通过overload与demeter链可部分绕过,但仍需phpunit.xml加processIsolation="true",在 GitLab CI 里会拖慢流水线。
答案
“我在日活 800W 的电商营销系统里同时用过两种方案,差异总结为‘三快三慢’:
- 写测试快:Mockery 一句链式就能完成‘桩+预期’,适合 Legacy 代码;
- 跑测试慢:PHPUnit 原生 mock 无代理层,1000 个用例能省 20 % 时间,适合核心支付域;
- 定位失败快:Mockery 的‘未满足期望’报错带参数快照,本地调试更直观;
- 接入 CI 慢:Mockery 必须在 tearDown() 手动 close,GitLab Runner 曾因此把失败率误标 3 %;
- 静态分析慢:Mockery 返回混合类型,Stan 级别 8 需要额外写 15 % 的
@var; - 升级维护快:PHPUnit 大版本升级时,原生 mock 零改动,Mockery 出现过 1.3→1.4 不兼容,需要锁版本。
因此我的选型策略是:新业务核心域用 PHPUnit 原生,保证性能;Legacy 模块、第三方 SDK 用 Mockery,降低重构成本;两者混用时加processIsolation="true"隔离,避免 final 类冲突。”
拓展思考
- 在 PHP 8 的
readonly类与enum场景下,两种框架都无法直接 mock,需要引入dg/bypass-finals或mockery/mockery#1163分支,你会如何评估引入新依赖的 ROI? - 国内很多银行接口仍用 SOAP + WSSE,Mockery 的
mockery\loader\EvalLoader在 Opcache Preloading 开启时会触发段错误,如何通过 Composer 插件自动降级到RequireLoader? - 如果团队规定“单测覆盖率 90 % 且耗时 <5 min”,而 Mockery 的 Expectation 反射导致超时,你会用 PHPUnit 的
createPartialMock()还是直接写“手写桩类”来兜底?请给出决策矩阵与灰度方案。