行业资讯
📅 2026/9/6 16:40:12
Coolify 测试实战:laravel-actions 的 AsFake 假对象体系与 Action 编排测试
Coolify 测试实战laravel-actions 的 AsFake 假对象体系与 Action 编排测试【免费下载链接】coolifyAn open-source, self-hostable PaaS alternative to Vercel, Heroku Netlify that lets you easily deploy static sites, databases, full-stack applications and 280 one-click services on your own servers.项目地址: https://gitcode.com/GitHub_Trending/co/coolify本文基于 Coolify 仓库中lorisleiva/laravel-actions包的 Action Fakes 参考文档完整覆盖AsFaketrait 提供的mock、partialMock、spy、shouldRun、shouldNotRun、allowToRun、isFake、clearFake全部假对象方法的语义与用法并结合 Coolify 源码中真实的 Action 类与 Pest 测试用例如 Stripe 订阅对账、资源迁移编排说明如何在真实大型 Laravel 项目中隔离 Action 编排、断言执行与否并避免 fake 泄漏。读完后你能掌握「只在被测边界做 fake」的两层测试策略并能写出可验证的分支覆盖测试。一、问题背景为什么需要 Action FakesCoolify 是一个自托管 PaaS其业务逻辑大量沉淀在App\Actions命名空间下——仓库中实际有 57 个类使用了Lorisleiva\Actions\Concerns\AsActiontrait如app/Actions/Stripe/SyncStripeSubscriptions.php、app/Actions/Service/StartService.php等依赖composer.json中声明的lorisleiva/laravel-actions版本约束^2.10.2。这些 Action 往往同时承担多种入口HTTP、队列 Job、事件监听器、Artisan 命令测试时面临两个诉求业务正确性handle(...)里的领域规则必须用真实依赖验证编排正确性上层入口Controller、Job、Command调用了哪些下游 Action、参数是否传对不能被下游的副作用SSH 连接、Docker 命令、外部 API拖垮。AsFake假对象体系就是为此设计的它允许你用一行静态调用把某个 Action 类替换为 mock/spy并声明「它应该或不应该被执行」。该参考文档的适用场景即「在测试中隔离 action 编排」isolating action orchestration in tests。二、推荐的测试模式两层策略参考文档给出的 Recommended pattern 是三条清晰原则Coolify 的测试组织方式与之完全一致直接测试handle(...)验证业务规则用工厂构造真实模型数据调用SyncStripeSubscriptions::run(...)后断言数据库最终状态测试入口entrypoint验证接线与编排例如执行 Artisan 命令、调用迁移编排方法只关心下游 Action 是否被正确调度只在被测边界处使用 fake不要把一切全 mock 掉否则测试失去对行为的信心这是文档 Common pitfalls 第一条。三、AsFake 全部方法详解以下逐一介绍AsFaketrait 提供的 8 个方法每个方法均附参考文档的标准示例及其语义边界。3.1mock()完整 mock将 Action 整体替换为一个 full mock所有方法都不再执行真实逻辑用于「严格期望 参数断言」的场景FetchContactsFromGoogle::mock() -shouldReceive(handle) -with(42) -andReturn([Loris, Will, Barney]);从源码结构看mock()底层构建的是 Mockery 风格的期望对象shouldReceive声明「期望被调用」with约束参数契约andReturn指定桩返回值。当被 mock 的 Action 在后续代码路径中被真实触发时若参数不匹配或未被调用而期望了调用测试会失败——这就是「fail fast」的交互契约。3.2partialMock()部分 mock保留大部分真实行为只替换其中一到两个昂贵/内部方法适合「行为大体真实、仅屏蔽一次外部调用」的场景FetchContactsFromGoogle::partialMock() -shouldReceive(fetch) -with(some_google_identifier) -andReturn([Loris, Will, Barney]);Coolify 测试中同样有类似的部分 mock 用法例如 ServiceTemplatesLastUpdatedHintTest.php 中对 Laravel 的Filefacade 使用File::partialMock()屏蔽文件系统读取——同一个「只截断一条昂贵路径」的思路。3.3spy()监视器spy 不预设严格期望而是「放行调用 事后验证」最适合先执行代码、再回答「它是否带着 X 参数被调用过」的问题$spy FetchContactsFromGoogle::spy() -allows(handle) -andReturn([Loris, Will, Barney]); // ... $spy-shouldHaveReceived(handle)-with(42);allows(handle)表示放行该方法并允许指定返回值shouldHaveReceived(...)是断言端可在执行完毕后的任意位置调用。3.4shouldRun()正向编排断言这是mock()-shouldReceive(handle)的语法糖一行代码即可完成「这个 Action 必须被执行」的声明FetchContactsFromGoogle::shouldRun(); // Equivalent to: FetchContactsFromGoogle::mock()-shouldReceive(handle);由于它返回的是期望链对象可以继续追加once()、with(...)、andReturn(...)等方法。Coolify 的真实用例见 SyncStripeSubscriptionsActionTest.phptest(the terminal command runs the reconciliation action synchronously, function () { SyncStripeSubscriptions::shouldRun() -once() -withArgs(fn (bool $fix, ?Closure $onProgress) $fix false $onProgress instanceof Closure) -andReturnUsing(function (bool $fix, Closure $onProgress): array { $onProgress(checking, 2, 10); return [ total_checked 0, discrepancies [], resubscribed [], errors [], fixed false, ]; }); $this-artisan(cloud:sync-stripe-subscriptions) -expectsOutputToContain(Checking stale subscriptions against Stripe... 2/10) -expectsOutput(Total subscriptions checked: 0) -assertSuccessful(); });这个用例完整展示了「入口测试」的形态Artisan 命令cloud:sync-stripe-subscriptions定义于 SyncStripeSubscriptions.php本身不含业务逻辑它只负责调用 SyncStripeSubscriptions Action。测试中用shouldRun()把 Action 换成桩-once()断言恰好调用一次-withArgs(fn ...)用闭包验证参数契约——默认不带--fix时fix false且传入了Closure形式的进度回调-andReturnUsing(...)的桩函数甚至主动调用了传入的$onProgress回调从而让命令输出2/10进度信息测试同时覆盖了「命令把 Action 的返回值渲染成终端输出」这段接线逻辑。而同一个文件里紧随其后的另一个用例则用[--fix true]断言了fix true分支的传参两者共同构成了对命令选项传递的完整参数化验证。3.5shouldNotRun()守卫子句与分支覆盖mock()-shouldNotReceive(handle)的语法糖用于声明「这段代码路径绝对不应触发该 Action」FetchContactsFromGoogle::shouldNotRun(); // Equivalent to: FetchContactsFromGoogle::mock()-shouldNotReceive(handle);Coolify 在 MigrateResourceToDestinationTest.php 中大量使用该模式验证资源迁移的守卫分支test(rejects migration to the same destination, function () { StopApplication::shouldNotRun(); $application createMigrateTestApplication($this); MigrateResourceToDestination::run($application, $this-destination, migrateVolumes: false); })-throws(ValidationException::class);这里被测编排是MigrateResourceToDestination::run(...)。当目标与源是同一目的地非法输入时编排应该在抛出ValidationException之前绝不触及StopApplication——用shouldNotRun()把这个「不应该发生」写成可执行断言比仅断言异常存在更强它同时锁死了「校验先于副作用」的执行顺序。该文件中共出现 7 处StopApplication::shouldNotRun()分别覆盖不同的拒绝分支构建服务器作为目标、卷迁移冲突等是典型的「分支覆盖」测试矩阵。3.6allowToRun()放行 事后断言spy 放行handle的组合糖。当你希望代码继续以桩返回值往下跑但事后还要验证交互时它比mock()更省事$spy FetchContactsFromGoogle::allowToRun() -andReturn([Loris, Will, Barney]); // ... $spy-shouldHaveReceived(handle)-with(42);与spy()-allows(handle)的差别在于意图表达allowToRun()语义上直接声明「允许它跑桩」而spy()更通用适合需要监视多个方法或动态决定是否放行的场合。3.7isFake()生命周期检查返回该类当前是否已被替换为 fake可用于写「元测试」或调试泄漏问题FetchContactsFromGoogle::isFake(); // false FetchContactsFromGoogle::mock(); FetchContactsFromGoogle::isFake(); // true3.8clearFake()清理与防泄漏清除已注册的 fake 实例。参考文档把「存在泄漏风险时清理 fake」列入 Checklist这在长测试文件中尤其重要一个测试里注册的 fake 若未清理可能污染同进程内后续测试的依赖解析。Coolify 的落地做法是在afterEach钩子里统一清理例如SyncStripeSubscriptionsActionTest.phpafterEach(function () { SyncStripeSubscriptions::clearFake(); });CheckDomainDnsJobTest.phpafterEach(fn () CheckDomainDns::clearFake());这种「文件级 afterEach 兜底清理 单用例按需注册」的组合使 fake 的生命周期严格限制在单个用例内即便用例中途失败也不会把 fake 带入下一个用例。四、参考文档的完整示例编排测试与守卫子句测试参考文档 Examples 一节给出了两类最典型的编排测试模板它们分别对应shouldRun/shouldNotRun两种断言方向4.1 编排测试正向it(runs sync contacts for premium teams, function () { SyncGoogleContacts::shouldRun()-once()-with(42)-andReturnTrue(); ImportTeamContacts::run(42, isPremium: true); });要点shouldRun()-once()-with(42)把「调用次数、参数值、返回值」三件事压进一行测试主体只有一个编排调用ImportTeamContacts::run(...)没有掺杂任何下游副作用。4.2 守卫子句测试反向it(does not run sync when integration is disabled, function () { SyncGoogleContacts::shouldNotRun(); ImportTeamContacts::run(42, integrationEnabled: false); });要点负向断言不需要andReturn因为它关心的是「没被调用」这件事本身。这类测试专门保护 guard clause——if (! $integrationEnabled) return;这一行代码若被误删本测试立即变红。这两类模板组合起来就构成了对一个分支两侧走/不走的完整覆盖正向用例锁死「该走时怎么走」反向用例锁死「不该走时绝不走」。五、参考文档的测试矩阵与方法选择参考文档 Recommended pattern 与 SKILL 层给出的实践默认值可以整理成一张选择表测试目标推荐方法适用场景业务规则正确性直接调用handle(...)真实依赖/工厂默认的第一层测试编排分支应调用shouldRun()读起来像自然语言断言分支测试首选编排分支不应调用shouldNotRun()guard clause、参数校验前置分支严格交互契约mock()参数/次数不匹配时 fail fast行为大体真实只截断一个方法partialMock()屏蔽昂贵或外部依赖方法放行执行 事后验证spy()/allowToRun()需要观察调用但允许流程继续防跨用例污染isFake()/clearFake()afterEach 兜底、元检查配套的入口级测试矩阵来自 SKILL 文档 Testing Guidance则规定了五类入口各自的验证方式HTTP 路由测试 fake 下游 ActionJob 测试 dispatch 后断言下游调用监听器测试 dispatch 事件后断言交互命令测试执行 artisan 后断言调用与输出业务规则测试直接调handle(...)。六、Checklist 与常见陷阱参考文档收尾部分给出的验收清单与陷阱列表直接映射到日常评审标准Checklist写完测试后自查断言验证的是「调用意图与参数契约」call intent and argument contracts而非仅仅「被调用了」存在泄漏风险时清理 fakeCoolify 的统一做法afterEach中clearFake()分支测试优先用shouldRun()/shouldNotRun()可读性优于裸mock()链。Common pitfalls两类典型反模式过度 mock把整条链路全换成假对象测试通过但你对真实行为毫无信心——fake 应只出现在被测边界而不是「一切」只断言 dispatch不断言业务正确性只验证「某个 Action 被分发了」却从未直接测试过它的handle(...)行为等于把业务正确性外包给了无人覆盖的代码。Coolify 的 SyncStripeSubscriptionsActionTest.php 恰好同时规避了两条陷阱前几个用例直接SyncStripeSubscriptions::run(fix: true)走真实handle(...)逻辑只 mock 掉StripeClient这一外部 HTTP 边界配合 Mockery 对 Stripe API 的精细期望如shouldReceive(retrieve)-with(sub_stale)、shouldNotReceive(retrieve)验证订阅对账的四种 resolution 分支delete_stale、manual_review、end_subscription等对应 SyncStripeSubscriptions.php 中的match表达式后两个用例才切换到shouldRun()只测 Artisan 入口的接线。业务层与入口层各得其所正是文档推荐模式在真实代码库中的完整落地。七、小结AsFake提供的 8 个方法覆盖了「替换、放行、断言、清理」四个环节选择依据只有一个这个测试要证明什么两层策略不可颠倒handle(...)直测业务入口测试只测编排fake 止步于被测边界正向分支用shouldRun()守卫分支用shouldNotRun()两者合力构成完整的分支覆盖以afterEachclearFake()兜底 fake 生命周期isFake()可用于泄漏排查可参考仓库中 tests/Feature/Subscription/SyncStripeSubscriptionsActionTest.php、tests/Feature/MigrateResourceToDestinationTest.php、tests/Feature/CheckDomainDnsJobTest.php 三个文件它们是本文模式在 Coolify 中的可直接阅读的范例方法级参考见 laravel-actions SKILL 文档其AsFake深度说明与本文同源。【免费下载链接】coolifyAn open-source, self-hostable PaaS alternative to Vercel, Heroku Netlify that lets you easily deploy static sites, databases, full-stack applications and 280 one-click services on your own servers.项目地址: https://gitcode.com/GitHub_Trending/co/coolify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考