尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Mockery 框架下 PHP 魔术方法(Magic Methods)的 Mock 实践与规避策略

Mockery 框架下 PHP 魔术方法(Magic Methods)的 Mock 实践与规避策略 Mockery 框架下 PHP 魔术方法Magic Methods的 Mock 实践与规避策略【免费下载链接】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导读在 Laravel 等 PHP 项目中使用 Mockery 编写单元测试时__set()、__get()、__call()等双下划线前缀的魔术方法Magic Methods既是强大的语言特性也是测试中最容易踩坑的雷区。本文以 Mockery 官方参考文档 magic_methods.rst 为核心骨架结合仓库内 Mockery 源码与测试用例系统讲解为什么单元测试应避免直接 Mock 魔术方法、如何转而测试它们所模拟的虚拟方法/虚拟属性以及__call()、__wakeup()、__toString()等特殊场景下的 Mockery 行为边界帮助你写出真正针对真实 API的可靠测试。一、背景什么是 PHP 魔术方法为什么它们给 Mock 带来麻烦PHP 中所有以双下划线__为前缀的方法统称为魔术方法Magic Methods常见的有__set($name, $value)向不可访问属性写入值时被调用__get($name)读取不可访问属性时被调用__call($method, array $args)调用不可访问方法时被触发实例上下文__callStatic($method, array $args)以静态方式调用不可访问方法时被触发__toString()对象被当作字符串使用时被调用__wakeup()反序列化时被调用。在单元测试与 Mock 对象的世界里这些方法带来一个特殊问题魔术方法是运行时通过拦截机制触发的它们在类中并不以真实方法/真实属性的形式存在。Mockery 官方文档明确指出见 magic_methods.rstPHP magic methods which are prefixed with a double underscore, e.g.__set(), pose a particular problem in mocking and unit testing in general.也就是说魔术方法本身不是一个稳定的可测 API——它们是语言层面的行为钩子。如果测试直接去断言__get()或__set()的调用实际上是在测试 PHP 语言的内部转发机制而不是在测试业务类的真实契约。二、核心原则测试虚拟方法与虚拟属性而不是魔术方法本身Mockery 给出的指导原则非常明确同样出自 magic_methods.rstIt is strongly recommended that unit tests and mock objects do not directly refer to magic methods. Instead, refer only to the virtual methods and properties these magic methods simulate.翻译成实战语言就是两条铁律不要在测试代码里直接出现__get()、__set()、__call()这类方法名只针对这些魔术方法所模拟出来的虚拟方法virtual methods和虚拟属性virtual properties编写期望expectations。例如一个类通过__get()实现了动态属性$user-email那么在测试中你就应该直接对email这个虚拟属性做断言而不是去 Mock__get()// 不推荐直接引用魔术方法 $mock-shouldReceive(__get)-with(email)-andReturn(devexample.com); // 推荐测试虚拟属性仿佛它就是真实声明的属性 $mock-shouldReceive(email)-andReturn(devexample.com);同样如果一个类通过__call()对外提供save()、find()等动态方法测试时应按普通方法处理// 不推荐 $mock-shouldReceive(__call)-with(find, [id 42]); // 推荐把虚拟方法当作真实方法对待 $mock-shouldReceive(find)-with(id, 42)-andReturn($user);遵循这一建议能带来两个直接收益测试的是类的真实 API你验证的是调用方真正依赖的公开接口虚拟方法/属性而不是实现细节魔术方法的转发逻辑避免与 Mockery 自身的魔术方法实现发生冲突Mockery 为了拦截方法调用与属性访问必然会在生成 Mock 时覆盖这些魔术方法。直接 Mock 魔术方法等于与框架的拦截机制抢饭碗很容易产生不可预期的行为。这一观点在 Mockery 的另一份参考文档 public_properties.rst 中得到了呼应Mockery does not support mocking any magic methods since these are generally not considered a public API.文档还提醒对于依赖__get()/__set()实现的虚拟属性请像它们在类中被真实声明一样去 Mock——这正是只针对虚拟属性原则的又一次强调。三、源码印证Mockery 是如何处理魔术方法的为了真正理解为什么不要直接 Mock 魔术方法我们来看仓库中 Mockery 的核心实现。3.1__call()是 Mock 对象的方法拦截中枢在 library/Mockery/Mock.php 中Mockery 生成的 Mock 基类自己实现了__call()/** * Capture calls to this mock */ public function __call($method, array $args) { return $this-_mockery_handleMethodCall($method, $args); } public static function __callStatic($method, array $args) { return self::_mockery_handleStaticMethodCall($method, $args); }也就是说Mockery 本身就是靠魔术方法__call()/__callStatic()来完成方法调用拦截的——所有对 Mock 上未真实声明方法的调用最终都会进入_mockery_handleMethodCall()去匹配你通过shouldReceive()注册的期望。这从源码层面证明了魔术方法正是 Mockery 的核心工作机制测试者再去Mock 魔术方法自然会产生语义冲突。3.2__toString()被转发到__call()同一个文件中Mock.php__toString()也被显式转发到了__call()/** * Forward calls to this magic method to the __call method */ public function __toString() { return $this-__call(__toString, array()); }这意味着即使你想 Mock__toString()它最终也会走统一的__call()拦截链路。这也解释了测试套件中为什么会存在testToStringMagicMethodCanBeMocked这样的用例见 tests/Mockery/ExpectationTest.php__toString()是可以被 Mock 的但它是被当作一个普通方法名经由__call()处理而不是作为独立的魔术方法机制。3.3__get()/__set()注释掉的历史实现在 Mock.php 中还存在一段被整体注释掉的__set()/__get()实现/**public function __set($name, $value) { $this-_mockery_mockableProperties[$name] $value; return $this; } public function __get($name) { if (isset($this-_mockery_mockableProperties[$name])) { return $this-_mockery_mockableProperties[$name]; } elseif(isset($this-{$name})) { return $this-{$name}; } throw new \InvalidArgumentException ( Property . __CLASS__ . :: . $name . does not exist on this mock object ); }**/从这段被注释的代码可以看出Mockery 曾经考虑过通过__get()/__set()来模拟属性读写但最终没有启用它。当前仓库版本更倾向于属性 Mock 应通过直接设置公共属性或使用set()/andSet()期望方法来完成正如 public_properties.rst 所述。注释代码的存在本身就是魔术方法容易与 Mock 机制冲突、需要谨慎处理这一结论的源码级佐证。四、延伸阅读与魔术方法相关的 Mockery 坑GotchasMockery 官方文档 gotchas.rst 记录了若干与魔术方法直接相关的限制理解这些能帮你避开最常见的测试陷阱4.1 含公共__wakeup()的类可 Mock但行为受限Classes containing public__wakeup()methods can be mocked but the mocked__wakeup()method will perform no actions and cannot have expectations set for it.包含公共__wakeup()方法的类可以被 Mock但 Mock 出来的__wakeup()不会执行任何动作也不能为它设置期望。原因是 Mockery 必须通过序列化/反序列化对象来规避某些__construct()的疯狂行为__construct()insanity若像普通方法一样 Mock__wakeup()会抛出BadMethodCallException。这是设计使然而非缺陷。4.2 依赖__call()的非真实方法必须先定义期望Classes using non-real methods, i.e. where a method call triggers a__call()method, will throw an exception that the non-real method does not exist unless you first define at least one expectation (a simpleshouldReceive()call would suffice).如果被测类通过__call()提供非真实方法non-real methods那么在没有先定义至少一个期望哪怕只是一个简单的shouldReceive()的情况下调用这些方法会抛出方法不存在异常。原因很直接Mockery 没有其他途径获知这个动态方法名必须先通过期望注册让它认识该方法。这一点在仓库测试中也有对应验证tests/Mockery/ContainerTest.php 等处定义了带__call($method, array $params)的测试辅助类用于验证这类动态方法场景下的 Mock 行为。4.3__callStatic()的边界不做 Mock测试 tests/Mockery/ContainerTest.php 中的testMockeryShouldNotMockCallstaticMagicMethod用例表明Mockery 对静态魔术方法__callStatic()采取不 Mock策略——这类静态魔术方法不在 Mockery 的拦截支持范围内测试中应同样遵循测试虚拟 API原则避免直接依赖。五、实战建议在 Laravel 项目中落地避开魔术方法的测试策略结合 magic_methods.rst 的结论与仓库源码给出可直接落地的检查清单代码审查时搜索魔术方法名在测试文件中搜索__get、__set、__call等字样发现直接引用即视为坏味道改为对虚拟方法/属性的期望把虚拟属性当作真实属性来 Mock例如 Eloquent 模型Laravel 中大量依赖__get/__set的典型场景的属性直接以属性名注册期望而不是 Mock 魔术方法为动态方法先注册shouldReceive()被测类若依赖__call()转发例如 Eloquent 的动态作用域、动态关系记得先声明至少一个期望否则会触发方法不存在异常区分可 Mock与应 Mock__toString()虽可 Mock但更推荐通过andReturn()直接返回字符串__wakeup()不可设置期望__callStatic()不在支持范围内——知道边界才能写出不踩坑的测试以真实 API为测试契约把调用方视角下暴露了什么方法/属性作为 Mock 的唯一依据魔术方法只是这些 API 的底层实现细节不应泄漏到测试代码中。六、小结结论一不要直接 Mock 魔术方法只测试它们所模拟的虚拟方法与虚拟属性——这是 Mockery 官方对__set()、__get()、__call()等方法的统一建议结论二这样做能保证测试针对类的真实 API并规避与 Mockery 自身拦截机制__call()/__callStatic()的冲突结论三源码层面Mock.php 的__call()与__toString()转发实现、被注释的__get()/__set()历史代码以及 gotchas.rst 中关于__wakeup()与__call()的限制记录共同构成了这条建议的底层依据结论四__toString()可 Mock、__wakeup()受限、__callStatic()不 Mock、动态方法需先注册期望——记住这四个边界你的 Mockery 测试就能在魔术方法横行的 PHP 世界里保持稳定与可读。进一步探索本文所有论证均可在仓库中复核相关源码与文档位于 samples/development-frameworks/laravel/vendor/mockery/mockery 目录下其中 magic_methods.rst、public_properties.rst、gotchas.rst 三份参考文档与 library/Mockery/Mock.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),仅供参考
返回列表