Mockery 与 PHPUnit 原生 Mock 差异

解读

在国内一线/二线互联网公司的 PHP 面试中,单元测试能力已从“加分项”变成“硬性门槛”。
面试官抛出“Mockery 与 PHPUnit 原生 Mock 差异”时,通常想验证三件事:

  1. 你是否真的写过测试(能说出痛点);
  2. 你是否理解“测试替身”的底层原理(隔离、验证、桩件);
  3. 你是否能在团队里做技术选型(性能、可读性、维护成本)。
    回答时切忌只罗列 API,而要结合业务场景、运行环境、CI 成本给出“什么时候用谁”的明确结论。

知识点

  1. 生成机制
    PHPUnit 原生 mock 使用继承+代码生成(eval)在内存中动态创建类;Mockery 同样代码生成,但代理层多包了一层“Expectation”对象,支持链式 API。
  2. 语法风格
    PHPUnit 采用“构建器”模式,方法桩与预期分开声明;Mockery 提供类 jQuery 的连贯接口,可以在一句代码里完成“桩+预期+返回值”。
  3. 验证粒度
    PHPUnit 只能验证“调用次数”与“参数等于”;Mockery 内置“hamcrest”匹配器,可验证“数组子集”“对象属性”“回调闭包”,对 Legacy 代码更友好。
  4. 性能差异
    在 PHP 7.4 + OPcache 环境下,PHPUnit 原生 mock 由于少一层代理,单测跑 1000 个用例约快 15 %~20 %;Mockery 的 Expectation 对象在复杂约束时会多 1 次反射调用。
  5. 集成成本
    PHPUnit 9 以后自带 registerMockObject(),可与 --strict-mark-output 无缝整合;Mockery 需要额外 Mockery::close() 放在 tearDown(),否则会出现“未满足期望”的延迟报错,CI 日志容易误判。
  6. 国内框架生态
    Laravel 默认自带 Mockery$this->mock() 助手函数),Hyperf、ThinkPHP 8 的单元测试骨架也是 phpunit.xml + Mockery 组合;Symfony 官方文档示例仍以 PHPUnit 原生 mock 为主。
  7. 静态分析友好度
    PHPUnit 生成的 mock 类带 @phpstan-type 标注,PhpStorm 可自动提示;Mockery 返回的是混合类型,需要额外写 @var 断言,否则 Stan 级别 6 以上会报“未找到方法”。
  8. 致命缺陷场景
    final/private 方法,PHPUnit 原生直接报错;Mockery 通过 overloaddemeter 链可部分绕过,但仍需 phpunit.xmlprocessIsolation="true",在 GitLab CI 里会拖慢流水线。

答案

“我在日活 800W 的电商营销系统里同时用过两种方案,差异总结为‘三快三慢’:

  1. 写测试快:Mockery 一句链式就能完成‘桩+预期’,适合 Legacy 代码;
  2. 跑测试慢:PHPUnit 原生 mock 无代理层,1000 个用例能省 20 % 时间,适合核心支付域;
  3. 定位失败快:Mockery 的‘未满足期望’报错带参数快照,本地调试更直观;
  4. 接入 CI 慢:Mockery 必须在 tearDown() 手动 close,GitLab Runner 曾因此把失败率误标 3 %;
  5. 静态分析慢:Mockery 返回混合类型,Stan 级别 8 需要额外写 15 % 的 @var
  6. 升级维护快:PHPUnit 大版本升级时,原生 mock 零改动,Mockery 出现过 1.3→1.4 不兼容,需要锁版本。
    因此我的选型策略是:新业务核心域用 PHPUnit 原生,保证性能;Legacy 模块、第三方 SDK 用 Mockery,降低重构成本;两者混用时加 processIsolation="true" 隔离,避免 final 类冲突。”

拓展思考

  1. 在 PHP 8 的 readonly 类与 enum 场景下,两种框架都无法直接 mock,需要引入 dg/bypass-finalsmockery/mockery#1163 分支,你会如何评估引入新依赖的 ROI?
  2. 国内很多银行接口仍用 SOAP + WSSE,Mockery 的 mockery\loader\EvalLoader 在 Opcache Preloading 开启时会触发段错误,如何通过 Composer 插件自动降级到 RequireLoader
  3. 如果团队规定“单测覆盖率 90 % 且耗时 <5 min”,而 Mockery 的 Expectation 反射导致超时,你会用 PHPUnit 的 createPartialMock() 还是直接写“手写桩类”来兜底?请给出决策矩阵与灰度方案。