示例工程数据库教程后端【免费下载链接】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),仅供参考
