Mockery 硬依赖 Mocking 实战:用 overload 前缀拦截 new 关键字创建的对象——基于 sql-server-samples 中 Mockery 源码的深度解析

发布时间:2026/9/24 13:46:14

Mockery 硬依赖 Mocking 实战:用 overload 前缀拦截 new 关键字创建的对象——基于 sql-server-samples 中 Mockery 源码的深度解析 示例工程数据库教程后端【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址https://gitcode.com/gh_mirrors/sq/sql-server-samples点击查看免费下载本文以 sql-server-samples 仓库所携带的 Mockery 依赖samples/development-frameworks/laravel/vendor/mockery/mockery/为事实依据系统讲解如何在不改动被测代码的前提下Mock 掉通过new关键字在方法内部直接实例化的硬依赖对象。读完本文你将掌握overload:前缀的实例 Mock 用法、理解其底层实现原理并学会配合 PHPUnit 进程隔离注解写出稳定通过的单元测试。一、什么是硬依赖为什么它难以 Mock在面向对象的 PHP 代码中依赖注入构造器注入、Setter 注入是提升可测试性的首选手段——依赖以参数形式传入测试时可以轻松替换为 Mock 对象。但现实中经常遇到如下风格的代码?php namespace App; class Service { function callExternalService($param) { $externalService new Service\External(); $externalService-sendSomething($param); return $externalService-getSomething(); } }这里Service\External是在方法内部直接通过new关键字创建的外部调用者包括测试代码无法在调用callExternalService()之前把真实对象替换成 Mock这种在类内部自行实例化、无法从外部注入的依赖就是典型的硬依赖Hard Dependency。Mockery 针对这种场景提供了专门的解决方案实例 MockInstance Mock。通过它可以让new \App\Service\External()这样的语句在实际执行时返回一个 Mock 对象从而做到零改动被测代码地完成测试。二、前置条件被测代码必须使用自动加载AutoloadingMock 硬依赖有一个硬性前提被测代码必须依赖自动加载autoloading机制来加载类而不是通过require/include显式引入类文件。这一要求并非空穴来风而是由 Mockery 的实现机制决定的Mockery 会顶替真实类来响应new调用如果代码是通过require显式加载的真实类文件会被 PHP 直接包含进当前进程Mockery 将无法拦截。Mockery 官方文档在 instance_mocking.rst 中同样明确指出这并不会阻止require语句引入真实类并触发 PHP 致命错误它适用于以自动加载作为主要类加载机制的场合。在 Laravel 这类现代框架中Composer 自动加载是标配vendor/autoload.php因此天然满足该前提。仓库中的 Laravel 示例项目同样遵循 Composer 依赖管理samples/development-frameworks/laravel/composer.json、composer.lock其中mockery/mockery正是以 Composer 包的形式被引入。三、核心用法overload:前缀创建实例 MockMockery 通过在 mock 名称前加overload:前缀来声明一个实例 Mock语法为m::mock(overload:App\Service\External);下面的测试示例演示了完整的用法——对App\Service::callExternalService()内部的new Service\External()进行拦截?php namespace AppTest; use Mockery as m; class ServiceTest extends \PHPUnit_Framework_TestCase { public function testCallingExternalService() { $param Testing; $externalMock m::mock(overload:App\Service\External); $externalMock-shouldReceive(sendSomething) -once() -with($param); $externalMock-shouldReceive(getSomething) -once() -andReturn(Tested!); $service new \App\Service(); $result $service-callExternalService($param); $this-assertSame(Tested!, $result); } }逐段拆解m::mock(overload:App\Service\External)声明对App\Service\External的实例 Mock。此后在该进程中执行new \App\Service\External()时得到的将是这个 Mock 实例而非真实对象shouldReceive(sendSomething)-once()-with($param)设置期望——sendSomething方法应被恰好调用一次且参数必须为$param即TestingshouldReceive(getSomething)-once()-andReturn(Tested!)设置期望——getSomething应被调用一次并返回Tested!$service new \App\Service()被测类本身照常实例化Service并未被 overload仍是真实类$this-assertSame(Tested!, $result)断言callExternalService()的返回值。由于内部的$externalService已被替换为 MockgetSomething()返回的正是 Mock 设置的Tested!。若上述期望未被满足例如sendSomething没有被调用、调用次数不对或参数不符Mockery 会在测试结束时抛出异常使测试失败——这正是 Mockery期望驱动测试风格的核心价值既验证行为结果又验证交互过程。四、源码级原理overload:前缀在 Mockery 内部如何被处理理解了用法之后我们深入 Mockery 源码确认其底层实现。在仓库的 Container.php 中mock()方法对字符串参数做前缀解析} elseif (is_string($arg) substr($arg, 0, 6) alias:) { $name array_shift($args); $name str_replace(alias:, , $name); $builder-addTarget(stdClass); $builder-setName($name); continue; } elseif (is_string($arg) substr($arg, 0, 9) overload:) { $name array_shift($args); $name str_replace(overload:, , $name); $builder-setInstanceMock(true); $builder-addTarget(stdClass); $builder-setName($name); continue; }关键差异一目了然alias:与overload:都会把 mock 命名为目标类名setName($name)并且以stdClass为生成基底二者唯一的区别是overload:额外调用了$builder-setInstanceMock(true)即把生成的 Mock 标记为实例 Mock。实例 Mock 的语义在 instance_mocking.rst 中有明确阐述实例 Mock 意味着$obj new \MyNamespace\Foo;这样的语句实际上会生成一个 Mock 对象。它通过用实例 Mock 替换真实类来实现与别名 Mock 类似……这让你可以在无法简单地注入替换对象的场景下拦截实例化。也就是说一旦用overload:声明了实例 Mock该进程中所有针对这个类名的new操作都会被掉包成 Mock 实例这正是callExternalService()内部那句new Service\External()能被拦截的根本原因。随后Mockery 通过 Loader 将生成的 mock 定义加载进当前进程。仓库中的默认加载器 EvalLoader.php 实现如下class EvalLoader implements Loader { public function load(MockDefinition $definition) { if (class_exists($definition-getClassName(), false)) { return; } eval(? . $definition-getCode()); } }注意class_exists(..., false)的第二个参数false——它表示不触发自动加载仅检查类是否已在当前进程中定义。这正是真实类文件绝不能已被加载这一约束的源码级体现若真实类已经被包含进进程类已存在Mockery 生成的同名 mock 类将无法被eval定义最终在 Container.php 中抛出异常if (class_exists($def-getClassName(), $attemptAutoload false)) { $rfc new \ReflectionClass($def-getClassName()); if (!$rfc-implementsInterface(Mockery\MockInterface)) { throw new \Mockery\Exception\RuntimeException( Could not load mock {$def-getClassName()}, class already exists ); } }即文档中所说的class already exists 异常。由此可以反推出一个清晰的因果链正因为 PHP 的类加载机制对同一类名只允许定义一次所以 overload 的类文件一旦被真实加载Mockery 就无法再顶替它。五、关键难点避免真实类被重复加载Mockery 能顶替真实类依赖一个脆弱前提——在生成 mock 的那一刻真实类尚未被加载。问题随之而来假如我们还想在别的测试中测试App\Service\External本身或者该类的真实文件在其他测试里已经被加载进同一进程那么再执行m::mock(overload:App\Service\External)就会触发上述 class already exists 异常。Mockery 官方文档给出的解决方案非常明确利用 PHPUnit 的进程隔离能力让包含 overload 类的测试在独立进程中运行并且不保留全局状态。这样真实类文件就不会跨越测试被重复包含到同一进程。具体做法是在测试类上添加两个 PHPUnit 注解/** * runTestsInSeparateProcesses * preserveGlobalState disabled */runTestsInSeparateProcesses让该测试类中的每个测试方法在独立的 PHP 子进程中执行从而保证每个测试进程内都是干净的类加载状态preserveGlobalState disabled关闭全局状态保留避免 PHPUnit 试图跨进程保存/恢复全局变量包括已经加载的类状态防止与 Mockery 的类顶替机制冲突。加入注解后完整示例变为?php namespace AppTest; use Mockery as m; /** * runTestsInSeparateProcesses * preserveGlobalState disabled */ class ServiceTest extends \PHPUnit_Framework_TestCase { public function testCallingExternalService() { $param Testing; $externalMock m::mock(overload:App\Service\External); $externalMock-shouldReceive(sendSomething) -once() -with($param); $externalMock-shouldReceive(getSomething) -once() -andReturn(Tested!); $service new \App\Service(); $result $service-callExternalService($param); $this-assertSame(Tested!, $result); } }运行该测试Mockery 会让App\Service内部使用被 Mock 掉的External服务测试正常通过。六、权衡与注意事项1. 性能代价进程隔离测试运行更慢文档明确提醒进程隔离有其缺点——这些测试会运行得更慢。每次测试都要启动一个全新的 PHP 子进程相比同进程内的普通测试有可观的开销。因此在实践中应只为确实使用overload:的测试类开启进程隔离不要把runTestsInSeparateProcesses无差别加到所有测试类上。仓库中 Mockery 自身的默认配置也印证了这一点——其 phpunit.xml.dist 中processIsolationfalse即默认不开启进程隔离只有个别需要 overload 的测试才通过注解按需开启。2. 一个进程内只能 overload 一次由于类只能定义一次同一个 overload 目标类在整个测试进程生命周期内只能被 mock 一次。如果两个测试用例在同一进程中重复 overload 同一个类后执行者会遭遇 class already exists 异常——这是进程隔离注解之所以必要的根本原因。3. 自动加载是硬前提如本文第二节所述被测代码若使用require/include显式加载类文件Mockery 无法保证拦截成功。使用overload:前请确认被测类及目标类都通过 Composer 自动加载。4. 源码与测试佐证Mockery 自身的测试套件中大量使用了overload:语法来验证该特性例如 tests/Mockery/ContainerTest.php 中的一系列用例$m $this-container-mock(overload:MyNamespace\MyClass4);这些用例既是该特性的回归测试也是学习overload:各种边界写法的现成参考资料包括overload:\MyNamespace\MyClass11这类带前导反斜杠的写法。如果你需要深入了解实例 Mock 的更多细节可直接阅读 instance_mocking.rst 与 phpunit_integration.rst。七、总结Mocking 硬依赖是单元测试中绕不开的实战场景Mockery 通过overload:前缀 实例 Mock 机制在不修改被测代码的前提下让new关键字创建的依赖对象变身为可控的 Mock。其背后的完整技术链条为overload:前缀被 Container.php 解析为实例 Mock标记生成的 Mock 类由 EvalLoader.php 以eval方式注入进程前提是目标类尚未被加载PHPUnit 的runTestsInSeparateProcessespreserveGlobalState disabled注解保证每个 overload 测试拥有独立的类加载环境避免 class already exists 冲突代价是进程级开销带来的测试速度下降故应精准、克制地使用。掌握了这套方法你就能在 Laravel 等自动加载环境下对那些内部new出来的硬依赖进行彻底的行为验证让难以测试的遗留代码重新回到可控的测试覆盖之下。赞分享示例工程数据库教程后端【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址https://gitcode.com/gh_mirrors/sq/sql-server-samples点击查看免费下载相关推荐SQL Server Samples 仓库中的 Laravel 示例Mockery PHP 模拟对象框架使用指南SQL Server Samples 仓库中的 Laravel 示例Mockery PHP 模拟对象框架使用指南 Mockery 是一个简洁而灵活的 PHP示例工程数据库教程后端sql-server-samples 之 Laravel 示例中的 Mockery 入门用 Simple Example 吃透 PHP Mock 测试sql server samples 之 Laravel 示例中的 Mockery 入门用 Simple Example 吃透 PHP Mock 测试 导读示例工程数据库教程后端Lama Cleaner一条命令完成 AI 图片擦除去水印、物体消除、老照片修复都在这里Lama Cleaner一条命令完成 AI 图片擦除去水印、物体消除、老照片修复都在这里 照片里的水印、闯入画面的路人、想擦掉的文字平时打开 Photos人工智能AI 应用计算机视觉图像处理媒体生成后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/24 14:46:24

Jackett索引器评分系统:告别资源选择困难症的终极解决方案

Jackett索引器评分系统:告别资源选择困难症的终极解决方案 你是否曾经面对数百个种子资源却不知如何选择?是否因为下载了低质量内容而浪费时间和带宽?Jackett的智能评分系统正是为你解决这一痛点的完美工具。作为开源项目Jackett的核心功能之…

2026/9/24 14:46:24

Hi3798MV300刷机实战:让CM201-2变身家庭电视安全中枢

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 14:46:24

冥想第两千一十一天(2011)

1.周二,项目上全力以赴的一天,今天游泳日,还是不会转体的时候借力,不过我会不断的练习。 2.感谢父母,感谢朋友,感谢家人,感谢不断进步的自己。

2026/9/24 14:41:23

shadcn-vue Dropdown Menu 组件完整指南:安装、API 与实战示例

UI组件前端 【免费下载链接】shadcn-vue Vue port of shadcn-ui 项目地址: https://gitcode.com/gh_mirrors/sh/shadcn-vue 点击查看 免费下载 本指南围绕 shadcn-vue 中的 Dropdown Menu(下拉菜单)组件展开,它是通过按钮等触发器…

2026/9/23 12:07:00

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/23 12:06:55

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/24 0:00:21

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:21

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:21

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/22 16:34:32

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/22 20:01:30

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/22 13:25:41

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码