ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Mockery 教程:在 Laravel 项目中模拟 Demeter 链与流式接口(Demeter Chains Mocking)

2026/9/24 10:49:40 拓冰建站 浏览量
Mockery 教程:在 Laravel 项目中模拟 Demeter 链与流式接口(Demeter Chains Mocking) 示例工程数据库教程后端【免费下载链接】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点击查看免费下载导读Demeter 链Demeter Chains与流式接口Fluent Interfaces是现代 PHP 开发中常见的调用模式$object-foo()-bar()-zebra()-alpha()-selfDestruct()这样的长串链式调用贯穿多个对象边界。Mockery 作为 PHP 领域最流行的 Mock 框架之一专门为这类场景提供了链式 Mock 支持允许你用一个-分隔的字符串直接声明整条调用链的期望并让最终一次调用返回指定值。本篇教程以仓库内 Laravel 示例所依赖的 Mockery 库文档为骨架结合 Mockery 源码 与其测试用例带你掌握 Demeter 链 Mock 的写法、底层实现原理与实战要点读完即可在真实测试中直接套用。什么是 Demeter 链与流式接口在 Mockery 的官方文档 demeter_chains.rst 中这两个术语指代的是同一种日益常见的调用方式——连续调用一串方法$object-foo()-bar()-zebra()-alpha()-selfDestruct();这种长链本身并不一定是坏事前提是链上的每一个方法调用都返回到调用方类所熟知的本地对象。一个有趣的佐证是Mockery 自身就大量使用长链——在第一次shouldReceive()调用之后后续的长链方法都作用在同一个\Mockery\Expectation实例上见 Expectation.php 中andReturn、times、ordered等方法链式返回$this的实现模式。但更多时候情况并非如此链上的每次调用都不断跨越对象边界前一个方法返回的是另一个完全不同的对象。此时若要在单元测试中逐个 Mock 链上的每一个中间对象将是一项令人头疼的工程——你需要为每一层创建一个 Mock、设置它的返回值为下一个 Mock、层层嵌套测试代码的复杂度会随链长线性膨胀。用-语法一键 Mock 整条链为了把逐步构造中间 Mock的噩梦变成一行代码Mockery 直接支持Demeter 链式 Mock在shouldReceive()中把整条调用链写成用-连接的字符串Mockery 会跳过中间环节直接从最后一次调用返回你定义的值。文档中的经典示例——假设selfDestruct()返回字符串Ten!给$object一个CaptainsConsole实例$mock \Mockery::mock(CaptainsConsole); $mock-shouldReceive(foo-bar-zebra-alpha-selfDestruct)-andReturn(Ten!);这里的期望expectation可以像任何其他期望一样继续追加andReturn()、times()、once()等约束唯一的区别是方法名变成了一串用-分隔的链式调用名。Mockery 会自动为整条链建立起完整的期望结构并挂上最终的返回值无论真实实现中链上经过多少中间对象。一个更贴近真实业务的例子假设你正在为某个订单服务写测试服务内部会执行$this-auth-user()-profile()-address()-city()这样的跨对象链$mock \Mockery::mock(App\Services\AuthService); $mock-shouldReceive(user-profile-address-city)-andReturn(Shanghai); $service new OrderService($mock); $this-assertEquals(Shanghai, $service-getUserCity());只需要一行声明user()、profile()、address()返回什么你完全不用关心Mockery 会保证city()最终返回Shanghai。需要留意的行为链上非末次调用的参数会被忽略这是 Demeter 链 Mock 的一个关键行为约束文档中明确说明Arguments to all members of the chain (except the final call) are ignored in this process.即链上除最后一次调用外的所有成员方法的参数在匹配时都会被忽略。因此上述shouldReceive(user-profile-address-city)无论调用方传user(a)还是user(b)Mock 都能命中除非你在最终调用上另行用with()约束参数。源码解析Mockery 是如何短路整条链的Demeter 链 Mock 并非魔法其实现完全可以在仓库源码中追踪。核心入口是 Mockery.php 中Mockery::parseShouldReturnArgs()第 618 行起它负责解析shouldReceive()传入的参数并生成期望其中对链式字符串的真正构建由buildDemeterChain()第 647 行起完成。整个流程如下拆链$methodNames explode(-, $arg)把foo-bar-zebra-alpha-selfDestruct拆成方法名数组。合法性校验如果 Mockery 配置禁止 Mock 不存在的方法mockingNonExistentMethodsAllowed()为假且首方法不在被 Mock 类的可 Mock 方法列表中会直接抛出\Mockery\Exception。逐层构建进入while (true)循环array_shift($methodNames)依次取出链上的方法若该层还没有期望且链上还有剩余元素则通过getNewDemeterMock()第 706 行在 Container 中创建一个名为demeter_{$method}的匿名 Mock并把当前期望的返回值指向这个新 Mock$exp-andReturn($mock)从而实现中间对象自动 Mock若链上只剩最后一个元素则把最终返回值挂到最末一层期望上并break。复用已存在的中间 Mock当链上某一层方法已经在之前出现过例如两条链共享同一前缀Container::getKeyOfDemeterMockFor($method)见 Container.php 第 252 行会通过正则preg_grep(/__demeter_{$method}$/, $keys)在 Container 注册表中查找已创建的__demeter_xxxMock 并复用避免重复生成对象。从源码结构看整个链式 Mock 的核心思想可以概括为把中间的每一层调用都短路为一个自动生成的、只负责把调用转发到下一层的占位 Mock让最终的andReturn()值直达调用方。这也正是文档所说regardless of whatever intermediary object might be used in the real implementation的底层保障。多个链式期望CompositeExpectation 的配合parseShouldReturnArgs()的返回值是\Mockery\CompositeExpectation当你在一次shouldReceive()中传入数组键为链字符串、值为返回值或多个链字符串时每个链都会被buildDemeterChain()单独构建期望再汇总进同一个 CompositeExpectation 中统一验证$mock-shouldReceive([ foo-bar-baz Spam!, a-b-c Ham!, ]);这种写法在 CompositeExpectation.php 中被聚合管理方便一次性声明多条链。测试用例佐证官方如何验证该特性Mockery 仓库自带测试完整覆盖了 Demeter 链场景见 tests/Mockery/ExpectationTest.php基础链 Mock第 1875 行testMockingDemeterChainsPassesMockeryExpectationToCompositeExpectationissue #20$mock $this-container-mock(Mockery_Demeterowski); $mock-shouldReceive(foo-bar-baz)-andReturn(Spam!); $demeter new Mockery_UseDemeter($mock); $this-assertSame(Spam!, $demeter-doit());测试辅助类Mockery_UseDemeter内部第 2021 行起执行$this-demeter-foo()-bar()-baz()最终断言拿到Spam!——验证了整条链被成功短路。带参数的链 Mock第 1886 行testMockingDemeterChainsPassesMockeryExpectationToCompositeExpectationWithArgs$mock-shouldReceive(foo-bar-baz)-andReturn(Spam!); // 调用方实际执行 foo(foo)-bar(bar)-baz(baz) $this-assertSame(Spam!, $demeter-doitWithArgs());这个用例正是链上非末次调用参数被忽略行为的最直接证据foo(foo)、bar(bar)都带了参数但 Mock 依然命中因为只有baz是最终调用。在 Laravel 项目中的实战建议本仓库的 Laravel 开发框架示例 中Mockery 作为vendor/mockery/mockery被完整引入composer.json中声明依赖因此以下场景在 Laravel 单元测试PHPUnit Mockery中可直接使用门面/服务跨层调用当被测对象通过auth()-user()-...、request()-user()-...这类跨对象链获取数据时用 Demeter 链 Mock 一行搞定避免为中间对象各自造 Mock。流式查询构造器如DB::table(...)-where(...)-orderBy(...)-get()这类链式查询用table-where-orderBy-get形式直接 Mock 最终结果。保持最终调用的参数约束如果需要区分最终调用的不同参数可在链字符串之外继续使用with()对最后一次调用做参数匹配中间层参数一律忽略测试要据此设计断言避免误以为参数不匹配是 bug。需要提醒的是该特性适用于 Mockery 的**期望Expectation**层面配合shouldReceive()使用链式写法让测试意图一目了然但请谨慎用于过长的链超过 45 层——链越长测试与被测实现的耦合越紧重构时维护成本也越高。小结Demeter 链与流式接口是跨对象边界的连续调用逐个 Mock 中间对象代价高昂Mockery 支持shouldReceive(a-b-c)-andReturn($value)的一键链式 Mock自动短路中间环节链上除最终调用外的参数一律忽略设计断言时需注意底层由Mockery::buildDemeterChain()配合 Container 的__demeter_*占位 Mock 实现并通过CompositeExpectation支持多条链的聚合声明官方测试用例ExpectationTest.php完整覆盖了基础链与带参数链两种场景可作为你测试写法的参照模板。如果你想深入了解 Mockery 的其他期望语法如times()、ordered()、passthru()等可以继续阅读 Mockery 文档目录 与 Expectation 实现。赞分享示例工程数据库教程后端【免费下载链接】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-mongodb测试模拟框架Mockery与MongoDB交互laravel mongodb测试模拟框架Mockery与MongoDB交互 在开发基于Laravel和MongoDB的应用时编写可靠的测试是确保代码质量的后端数据库Buzz离线转录实战指南从本地跑通到批量交付字幕的全流程Buzz离线转录实战指南从本地跑通到批量交付字幕的全流程 Buzz 是一款运行在你自己电脑上的离线音频转录工具基于 OpenAI Whisper 模型把录人工智能语音音频本地部署桌面应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考