- 示例工程
- 数据库
- 教程
- 后端
【免费下载链接】sql-server-samples
Azure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge
本文基于 sql-server-samples 仓库中 Laravel 开发框架示例项目(samples/development-frameworks/laravel)所携带的 Mockery 测试框架文档,深入讲解 Mockery 的全局配置对象及其三大核心行为。读者将掌握通过\Mockery::getConfiguration()精细调控"是否允许 Mock 不存在的方法"、"是否允许未被满足的期望"以及"为 PHP 内部类方法手工声明参数映射"三项能力,并结合仓库源码与测试用例理解其底层生效机制,从而在 PHPUnit 测试套件中写出更严谨、更可维护的 Mock 测试。
Mockery 全局配置:一个单例,三项开关
Mockery 允许对部分核心行为进行"细粒度微调",其实现方式是一个单例配置对象(singleton configuration object)。在仓库携带的 Mockery 源码中,这个对象就是 library/Mockery/Configuration.php 中定义的Mockery\Configuration类,它持有当前配置的三项核心行为:
- 是否允许 Mock 实际并不存在的方法(默认开启);
- 是否允许存在从未被满足(即未被使用)的期望(默认开启);
- 为 PHP 内部类方法设置/读取参数映射——因为
Reflection无法自动检测内部类方法的参数签名。
该单例由 library/Mockery.php 中的\Mockery::getConfiguration()静态方法惰性创建并返回:首次调用时实例化new \Mockery\Configuration()并缓存,此后所有调用共享同一配置实例。这意味着你在任意测试中修改配置,会立即影响整个测试进程内的所有 Mock 行为。
默认情况下,前两项行为都是启用的。不过文档明确指出,这种宽松默认值在某些场景下会带来"非预期后果":
- 允许 Mock 不存在的方法,可能让基于真实类/对象建立的 Mock 与真实实现"脱节"——尤其当测试没有进行一定程度的集成测试(对对象装配关系进行验证)时,这类偏差很难被发现;
- 允许未被满足的期望,意味着多余的 Mock 期望不会被察觉,从而污染测试代码,并可能误导阅读测试的人。
开关一:是否允许 Mock 不存在的方法
\Mockery::getConfiguration()->allowMockingNonExistentMethods(bool);传入true允许该行为,传入false禁止该行为。两者都会立即生效,直到再次调用切换回来。一旦在禁止状态下检测到"Mock 了不存在的方法",Mockery 会在该处直接抛出Exception。
源码中的默认值与存取逻辑
在 Configuration.php 中:
- 受保护属性
_allowMockingNonExistentMethod默认值为true; allowMockingNonExistentMethods($flag = true)将传入值强制转换为布尔并写入;- 读取端由
mockingNonExistentMethodsAllowed()返回当前标记。
这一开关在哪里真正生效
从源码调用链看,该标记在三个关键位置被消费:
Mock 初始化阶段:在 library/Mockery/Mock.php 的
mockery_init()中,若禁止 Mock 不存在方法,Mock 对象会把自身通过mockery_getMethods()获取到的真实公共、非静态方法名收集进_mockery_mockableMethods数组,作为后续"可 Mock 方法白名单"。魔术调用阶段:同样在 Mock.php 附近,调用不存在方法时,若禁止标志开启且方法既不存在于 partial 对象、也不可调用
parent::$method,则该调用会被拦截。Demeter 链构建阶段:在 Mockery.php 的
buildDemeterChain()中,当禁止 Mock 不存在方法、且当前 Mock 不是匿名 Mock、且链首方法不在mockery_getMockableMethods()列表中时,会抛出Mockery\Exception,错误信息明确提示:"Mockery's configuration currently forbids mocking the method ... as it does not exist on the class or object being mocked"。
测试用例佐证
仓库测试文件 tests/Mockery/MockTest.php 用多个用例验证了这一开关的行为:
testShouldIgnoreMissingDisallowMockingNonExistentMethodsUsingGlobalConfiguration:先allowMockingNonExistentMethods(false),再对真实类ClassWithMethods的 Mock 调用shouldReceive('nonExistentMethod'),预期抛出Mockery\Exception;testShouldIgnoreMissingCallingNonExistentMethodsUsingGlobalConfiguration:同样的禁止状态下直接调用$mock->nonExistentMethod(),预期抛出BadMethodCallException;testShouldIgnoreMissingCallingExistentMethods:禁止状态下调用真实存在的方法foo()、bar()仍可正常返回,说明该开关只拦截"不存在的方法";testMockWithNotAllowingMockingOfNonExistentMethodsCanBeGivenAdditionalMethodsToMockEvenIfTheyDontExistOnClass:即便全局禁止,仍可通过shouldAllowMockingMethod('testSomeNonExistentMethod')显式放行单个不存在的方法。
值得留意的是,testAnonymousMockWorksWithNotAllowingMockingOfNonExistentMethods表明匿名 Mock(不基于任何类)不受此限制——这正与buildDemeterChain()中!$mock->mockery_isAnonymous()的判断逻辑相互印证:匿名 Mock 没有"真实类"可参照,自然谈不上"不存在的方法"。
开关二:是否允许未被满足的期望
\Mockery::getConfiguration()->allowMockingMethodsUnnecessarily(bool);同样的语义:true允许,false禁止,立即生效。该行为针对的是"设置了期望(例如使用了zeroOrMoreTimes()这类零次或多次约束),却从未被调用"的冗余期望。默认true时这些冗余期望会被静默忽略;一旦禁止,检测到未使用的期望就会抛出Exception。
在源码 Configuration.php 中,对应属性_allowMockingMethodsUnnecessarily默认值为true,写入端allowMockingMethodsUnnecessarily($flag = true)同样做了布尔强制转换,读取端为mockingMethodsUnnecessarilyAllowed()。
启用这一约束后,测试代码中"写了却永远用不到的期望"会被立即揪出,促使开发者清理冗余断言,让测试意图更清晰、更贴近被测对象的真实交互。文档特别提醒:禁用这两项行为都应谨慎权衡,因为它们必然会削减去 Mockery 的一部分灵活性——例如过度依赖真实类方法存在性检查,会让以"宽松 Mock"著称的测试风格变得寸步难行。
能力三:内部类方法的参数映射(Reflection 的盲区补丁)
\Mockery::getConfiguration()->setInternalClassMethodParamMap($class, $method, array $paramMap); \Mockery::getConfiguration()->getInternalClassMethodParamMap($class, $method);这两个方法用于为PHP 内部类(internal classes,例如 SPL 类,或 PECL 扩展类如 ext/mongo 的MongoCollection)的方法定义参数签名。原因是 PHP 的Reflection无法分析内部类方法的参数,Mockery 也就无法得知这些方法的参数信息。绝大多数情况下你根本不需要用到它;它主要出现在"内部类方法的某个参数是按引用传递(pass-by-reference)"的场景——此时必须确保参数签名中正确包含&符号,因为 Mockery 无法为内部类自动补上这个符号。
源码实现细节
在 Configuration.php 中:
setInternalClassMethodParamMap($class, $method, array $map)将映射写入_internalClassParamMap,类名与方法名都会被strtolower()归一化后存储,因此调用时大小写不敏感;getInternalClassMethodParamMap($class, $method)按同样的小写规则读取,未命中时返回null;- 配套提供
resetInternalClassMethodParamMaps()可清空全部已覆盖的参数映射,以及getInternalClassMethodParamMaps()返回整个映射数组。
映射如何流入 Mock 生成流程
参数映射的消费点在 library/Mockery/Container.php 的mock()方法中:创建MockConfigurationBuilder后,立即调用$builder->setParameterOverrides(\Mockery::getConfiguration()->getInternalClassMethodParamMaps()),将全局配置中的内部类参数映射注入构建器,从而影响后续 Mock 类的生成与方法的签名定义。
实战示例:MongoCollection::insert() 的按引用参数
仓库文档 pass_by_reference_behaviours.rst 给出了完整示例。MongoCollection是 PECL mongo 扩展提供的内部类,其insert()方法第一个参数是数据数组,第二个是可选选项数组;原始数据数组会被更新——即insert()含有一个按引用传递的参数,调用后数据中会新增_id字段。Mockery 无法用 Reflection 分析这一点,因此需要手工配置参数映射:
\Mockery::getConfiguration()->setInternalClassMethodParamMap( 'MongoCollection', 'insert', array('&$data', '$options = array()') ); $m = \Mockery::mock('MongoCollection'); $m->shouldReceive('insert')->with( \Mockery::on(function(&$data) { if (!is_array($data)) return false; $data['_id'] = 123; return true; }), \Mockery::any() ); $data = array('a'=>1,'b'=>2); $m->insert($data); $this->assertTrue(isset($data['_id'])); $this->assertEquals(123, $data['_id']); \Mockery::resetContainer();注意三点实操要点:
- 参数签名数组中,
'&$data'的&必须显式写出,这是 Mockery 无法自动补充的关键信息; - 通过
\Mockery::on()闭包参数匹配器模拟"按引用修改":闭包接收&$data,在内部写入_id字段,使外部变量$data同步被修改; - 测试结束后调用
\Mockery::resetContainer()清理容器与配置状态,避免影响后续测试。
这段代码同时是仓库 Mockery.php 中parseShouldReturnArgs与Expectation机制配合工作的典型范例:with()中多个参数依次用\Mockery::on()闭包和\Mockery::any()匹配器进行校验。
配置的作用范围、生命周期与 Laravel 实践建议
由于配置对象是全局单例且改动立即生效,在实际测试中需要特别注意以下几点:
- 作用范围:所有三项配置均作用于整个测试进程,而非单个 Mock 或单个测试方法;
- 生命周期:配置在进程内持续累积,若在某测试中关闭了某行为,需在测试结束后显式恢复(仓库测试如 MockTest.php 的模式即是
allowMockingNonExistentMethods(false)用完后立即...->allowMockingNonExistentMethods(true)还原,或直接\Mockery::resetContainer()); - 优先考虑局部约束:全局开关通常适合"整批测试套件"级别的统一约束(例如在
setUp()或测试基类中统一关闭某行为);若只想约束单个 Mock,优先使用shouldAllowMockingMethod()、shouldAllowMockingProtectedMethods()等局部 API,避免全局状态污染; - 按引用参数是硬性需求:凡是涉及内部类方法的按引用参数 Mock,
setInternalClassMethodParamMap()不是可选项而是必选项,否则生成的 Mock 方法签名与真实方法不一致,行为将无法正确模拟。
在 Laravel 项目中,Mockery 通常经 PHPUnit 的测试监听器与Mockery\Adapter\Phpunit集成(见仓库 vendor 目录下 MockeryPHPUnitIntegration.php)。当你需要"严禁 Mock 幽灵方法"或"杜绝冗余期望"这类团队级测试纪律时,可将\Mockery::getConfiguration()的开关集中放置在测试基类的初始化逻辑中统一启用,再通过上述局部 API 按需放行,即可在保留 Mockery 灵活性的同时获得更严格的测试保障。
小结
Mockery 的全局配置对象通过三个精炼的 API 族,将"宽松"与"严谨"之间的尺度交还给了测试编写者:allowMockingNonExistentMethods()控制 Mock 与真实类之间的同步纪律,allowMockingMethodsUnnecessarily()控制测试代码中的冗余期望,setInternalClassMethodParamMap()/getInternalClassMethodParamMap()则弥补了 PHP Reflection 对内部类方法的分析盲区。理解它们各自在 Configuration.php、Mock.php、Container.php 与 Mockery.php 中的生效路径,以及 MockTest.php 中的行为验证,是写出既灵活又严谨的 PHP 单元测试的关键一步。
- 示例工程
- 数据库
- 教程
- 后端
【免费下载链接】sql-server-samples
Azure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge
相关推荐
Mockery 默认期望(Default Mock Expectations):用 byDefault() 消灭重复的 Mock 配置
Mockery 默认期望(Default Mock Expectations):用 byDefault 消灭重复的 Mock 配置 导读 在 PHP 单元测试中
测试开发工具Mockery 全局配置(Global Configuration)完全指南:细粒度控制 mock 行为的官方开关
Mockery 全局配置(Global Configuration)完全指南:细粒度控制 mock 行为的官方开关 Mockery 通过一个单例(singlet
测试开发工具Escrcpy 偏好设置完全指南:从全局配置到 Scrcpy 参数映射
Escrcpy 偏好设置完全指南:从全局配置到 Scrcpy 参数映射 Escrcpy 是一款基于 Scrcpy 的跨平台 Android 设备控制工具。本篇指
桌面应用移动开发开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考