ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Mockery 处理 final 类与 final 方法:Proxy Mock 代理 Mock 原理与 Laravel 测试实战

Mockery 处理 final 类与 final 方法:Proxy Mock 代理 Mock 原理与 Laravel 测试实战 Mockery 处理 final 类与 final 方法Proxy Mock 代理 Mock 原理与 Laravel 测试实战【免费下载链接】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在 PHP 单元测试中final关键字是 Mockery 这类 Mock 框架最头疼的限制之一它禁止子类覆盖方法而 Mockery 恰恰依赖生成子类来实现 Mock。本文基于本仓库 Laravel 示例samples/development-frameworks/laravel所携带的 Mockery 文档final_methods_classes.rst完整讲解 Mockery 应对 final 类与 final 方法的代理 MockProxy Mock方案何时可用、何时受限、底层是如何实现的以及如何在 Laravel 项目中安全落地。读完本文你将掌握面对final class/final method时最稳妥的 Mock 策略并能从源码与测试层面验证其行为边界。为什么final是 PHP Mock 框架的天敌Mockery 生成 Mock 对象的基本手段是子类化subclassing为被 Mock 的类生成一个子类并覆盖其中的方法从而拦截调用、注入预期行为expectations。这决定了两个硬约束被final修饰的类不能被继承因此无法生成子类来覆盖方法被final修饰的方法不能在子类中被重写因此即使类本身可继承该方法也无法被替换。final关键字的价值在于类型安全与防篡改但这与运行时替换行为的 Mock 需求天然冲突。因此Mockery 官方文档给出的最简单解决方案是不要将类或方法标记为final。但在类型安全与 Mock 能力之间必须妥协时Mockery 提供了Proxy Mock代理 Mock作为折中方案。Proxy Mock把实例对象传给\Mockery::mock()当目标类本身被标记为final或类中包含final方法时不能通过类名生成子类但可以传入一个已实例化的对象来创建代理 Mock// 传类名类为 final 时会抛异常详见下文源码分析 // $mock \Mockery::mock(SomeFinalClass); // 抛出 Mockery\Exception // 传实例对象生成 Proxy Mock一切照常工作 $mock \Mockery::mock(new SomeFinalClass); // 对代理 Mock 设置预期与普通 Mock 完全一致 $mock-shouldReceive(foo)-andReturn(baz); $result $mock-foo(); // baz其工作原理是Mockery 不再继承目标类而是生成一个Proxy代理指向你传入的真实对象对于设置了预期的方法调用代理拦截并返回预期结果对于未设置预期的方法调用代理将调用转发reroute给真实对象执行。关键代价代理 Mock 不继承被 Mock 对象的类类型即不会通过instanceof检查。若代码中存在类型约束typehint例如函数签名要求SomeFinalClass $obj传入代理 Mock 将失败。这正是文档中Mocking functionality 与 type safety 之间的 compromise妥协的含义。在 Laravel 测试中的典型用法在 Laravel 项目中当需要 Mock 一个来自第三方包、被final修饰的服务类时可以通过容器绑定实例$realService new FinalPaymentGateway($config); $proxy \Mockery::mock($realService); $proxy-shouldReceive(charge)-once()-andReturn(true); $this-app-instance(FinalPaymentGateway::class, $proxy); // 此时业务代码中的 $this-app-make(...) 会拿到代理 Mock需要注意的是若业务代码对FinalPaymentGateway有构造器类型约束constructor typehint代理 Mock 因不满足instanceof而无法注入——此时应优先考虑重构去掉final或引入接口而不是强行 Mock。源码级原理Mockery 如何拒绝与退让本仓库的 Mockery 源码library/Mockery清晰展示了这套策略的完整实现链。1. 类为final直接抛异常并提示代理方案在 MockConfiguration.php 的getTargetClass()中当按类名解析目标类时一旦发现其为final且没有传入实例对象立即抛出异常if ($this-getTargetObject() false $dtc-isFinal()) { throw new \Mockery\Exception( The class . $this-targetClassName . is marked final and its methods . cannot be replaced. Classes marked final can be passed in . to \Mockery::mock() as instantiated objects to create a . partial mock, but only if the mock is not subject to type . hinting checks. ); }注意异常信息中的三个要点① 按类名 Mockfinal类被明确拒绝② 补救方式是传入已实例化对象③ 前提是不涉及类型检查——即无法通过instanceof/typehint与文档结论完全一致。2. 生成阶段final类跳过extends在代码生成管线 ClassPass.php 中Pass 负责把生成的 Mock 代码从implements MockInterface改写为extends \TargetClass implements MockInterface但当目标类是final时直接短路返回不生成继承关系if ($target-isFinal()) { return $code; }这正是代理 Mock 无法通过instanceof检查的根因——生成的 Mock 类与目标类之间根本没有继承链。3. 方法筛选final方法被直接排除在 MockConfiguration.php 的getMethodsToMock()中所有候选方法先经过一轮过滤foreach ($methods as $key $method) { if ($method-isFinal()) { unset($methods[$key]); } }final方法不会进入MethodDefinitionPassMethodDefinitionPass.php的方法覆盖清单因此对含final方法的类类本身非final仍可通过类名 Mock只是final方法保持原样——这对应文档与源码中自动生成的内部 partialinternal partial场景。4. 实例对象如何进入 Mock 配置在 Container.php 的参数解析中当传入的参数是对象时它被当作 partial 目标加入 builder} elseif (is_object($arg)) { $partial array_shift($args); $builder-addTarget($partial); continue; }随后 MockConfiguration.php 的getTargetObject()返回该真实对象isInstanceMock()L400-L403标记其为实例 Mock并驱动 InstanceMockPass.php 在生成的类中注入构造逻辑从关联的真实对象拷贝属性、克隆其预期expectations实现代理转发的基础设施。5. 运行时未匹配预期的调用转发给真实对象在 Mock 基类 Mock.php 中保存真实对象的字段注释直接点明其职责/** * Instance of a core object on which methods are called in the event * it has been set, and an expectation for one of the objects methods * does not exist. This implements a simple partial mock proxy system. */ protected $_mockery_partial null;调用未命中任何预期时运行时分派逻辑会走_mockery_deferMissing/_mockery_ignoreMissing分支Mock.php对代理 Mock 而言未设置预期的调用被延后defer到真实对象执行从而让只 Mock 几个方法、其余照常的局部 Mock 语义成立。测试用例验证行为边界的实证仓库测试 ContainerTest.php 用一组针对性用例锁定了这些行为可直接作为行为契约阅读场景 Afinal类按类名 Mock 抛异常L260-L263配合测试夹具final class MockeryFoo3L1377-L1383public function testMockingAKnownConcreteFinalClassThrowsErrors_OnlyPartialMocksCanMockFinalElements() { $m $this-container-mock(MockeryFoo3); // 抛 \Mockery\Exception }场景 Bfinal类可通过传入实例创建代理 Mock且不通过instanceofL273-L279public function testFinalClassesCanBePartialMocks() { $m $this-container-mock(new MockeryFoo3); $m-shouldReceive(foo)-andReturn(baz); $this-assertEquals(baz, $m-foo()); $this-assertFalse($m instanceof MockeryFoo3); // 关键断言不是同一类型 }场景 C含final方法的类可用类名 Mock也可代理 MockL298-L322夹具为class MockeryFoo4含final public function foo()L1385-L1396// 代理 partial传入实例foo() 是 final 但仍可被 Mock因为代理不受 final 限制 $m $this-container-mock(new MockeryFoo4); $m-shouldReceive(foo)-andReturn(baz); $this-assertEquals(baz, $m-foo()); $this-assertEquals(bar, $m-bar()); // 未 Mock 的方法转发给真实对象 $this-assertTrue($m instanceof MockeryFoo4); // 类本身非 final仍满足 instanceof // 传统 partialMockeryFoo4[bar] 可 Mock bar() $m $this-container-mock(MockeryFoo4[bar]); // 但 MockeryFoo4[foo] 无法 partial 化 final 方法 // 期望被忽略foo() 仍返回真实实现 baz $m $this-container-mock(MockeryFoo4[foo]); $m-shouldReceive(foo)-andReturn(foo); $this-assertEquals(baz, $m-foo()); // final 方法不可覆盖断言返回真实值场景 C 揭示了三个层次的结论代理 Mock 能间接 Mockfinal方法因为代理类与目标类无关传统 partial 对final方法的期望会被静默忽略对应getMethodsToMock()的过滤逻辑类本身非final时即使走代理instanceof依然成立。场景 DSPL 内置类同样适用L281-L296SplFileInfo含 public final 方法仍可按类名 Mock 且满足instanceof SplFileInfo——这正是内部自动 partial策略含final方法的类final方法保持原样、其余方法可 Mock的典型代表。这也是 partial_mocks.rst 中第四种内部 partial所指的场景。与 Partial Mock 体系的关系本主题与 Mockery 的 Partial Mock 机制紧密相关详见仓库文档 partial_mocks.rst。Mockery 共提供四种 partial 策略其中三种面向用户策略用法是否受final影响instanceof 是否成立传统 Partial\Mockery::mock(MyClass[foo,bar])final方法无法被 partial 化成立类非 final 时被动 Partial\Mockery::mock(MyClass)-makePartial()同左成立代理 PartialProxy Mock\Mockery::mock(new MyClass)可绕过final限制不成立第四种为框架内部自动生成当你按类名 Mock 一个含final方法的类时Mockery 自动将这些方法排除在覆盖范围外形成内部 partial无需也无法手动指定。若确实必须 Mock 某个final方法代理 Mock 是唯一途径且必须接受instanceof失效的代价。工程实践建议与限制总结在 Laravel或其他 PHP 项目中使用 Mockery 面对final代码时建议按以下顺序决策首选移除final。若类/方法没有硬性安全需求去掉final是最简单、最无副作用的方案Mockery 文档亦明确推荐次选引入接口。将final类的公共行为抽象为接口让业务代码依赖接口而非具体类测试时 Mock 接口即可兼顾类型安全与可测性兜底代理 Mock。对无法修改的第三方final类使用\Mockery::mock($instance)创建代理但务必确认被测代码没有对该类的构造器/参数类型约束否则注入会失败避免不要指望对final方法的期望能被传统 partial 尊重——它会被静默忽略且不会报错容易造成测试看似通过、实际未生效的假象。适用前提与限制本文所述行为基于本仓库 vendor 目录中携带的 Mockery 版本samples/development-frameworks/laravel/vendor/mockery/mockery其composer.json定义了版本约束README 位于 README.md。核心结论——代理 Mock 可 Mockfinal类/方法但不满足instanceof——由源码 MockConfiguration.php 与测试 ContainerTest.php 共同保证是稳定且可验证的行为契约。【免费下载链接】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创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表