如何用PHPUnit 10 Mock Laravel Cashier的Billable trait checkout方法?
问题分析与解决
你的Mock失败核心原因是没理清Cashier链式调用的对象关系,以及Mock时的参数匹配、对象实例指向错误:
错误点拆解
直接Mock User类的链式方法无效
newSubscription、trialUntil、allowPromotionCodes这些方法并非User类本身的方法:newSubscription是Billable trait提供的,调用后返回SubscriptionBuilder实例,后续的链式方法都是SubscriptionBuilder的方法,所以直接在User的partial mock上期望这些链式调用完全不生效。Mock SubscriptionBuilder时的参数匹配问题
你之前的写法里存在几个潜在问题:- 参数名不匹配:代码中
newSubscription的参数是$name和$price,但Mock时写的是with($product, $price),变量名对应错误会导致Mock不触发。 - 未匹配
trialUntil和checkout的参数:如果Mock时没有指定参数(或参数不匹配),链式调用会因为方法调用不匹配而中断。 - 可能未正确引入
SubscriptionBuilder的命名空间:必须确保Mock的是Laravel\Cashier\SubscriptionBuilder类。
- 参数名不匹配:代码中
正确Mock写法
以下是能正确覆盖你业务逻辑的Mock代码:
use Laravel\Cashier\SubscriptionBuilder; use Laravel\Cashier\Checkout; use Carbon\Carbon; // 1. Mock Checkout实例,返回预设的结账URL $checkoutMock = Mockery::mock(Checkout::class); $checkoutMock->url = 'https://chicken.com'; // 2. Mock SubscriptionBuilder,处理链式调用 $subscriptionBuilderMock = Mockery::mock(SubscriptionBuilder::class); // 匹配trialUntil的参数(用type匹配Carbon实例,避免严格时间匹配) $subscriptionBuilderMock->shouldReceive('trialUntil') ->with(Mockery::type(Carbon::class)) ->andReturnSelf(); // allowPromotionCodes无参数,直接返回自身 $subscriptionBuilderMock->shouldReceive('allowPromotionCodes') ->andReturnSelf(); // 匹配checkout的参数(如果需要严格匹配数组,可传入完整数组;用any()则忽略参数细节) $subscriptionBuilderMock->shouldReceive('checkout') ->with(Mockery::any()) ->andReturn($checkoutMock); // 3. Mock User类,指定newSubscription返回Mock的SubscriptionBuilder $userMock = Mockery::mock(User::class)->makePartial(); // 注意这里的参数要和你业务代码中传入的$name、$price一致,用any()可忽略参数 $userMock->shouldReceive('newSubscription') ->with(Mockery::any(), Mockery::any()) ->andReturn($subscriptionBuilderMock);
额外注意事项
- 如果需要严格匹配
checkout的参数数组,可以把Mockery::any()替换为实际的参数数组,比如:->with([ 'success_url' => $expectedSuccessUrl, 'cancel_url' => $expectedCancelUrl, 'consent_collection' => ['terms_of_service' => 'required'], ]) - 确保测试类引入了Mockery的PHPUnit集成,比如在测试类顶部添加:
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; - 测试结束后要清理Mockery,可在
tearDown方法中添加:protected function tearDown(): void { parent::tearDown(); Mockery::close(); }
内容的提问来源于stack exchange,提问作者whoacowboy
相关产品推荐
相关产品推荐

