Laravel:如何用Mockery正确模拟服务绑定解决未生效问题
问题描述
现有如下业务类:
class Testclass { public WebshopappApiClient $client; public function __construct(Webshop $webshop) { $this->client = app()->make(WebshopappApiClient::class, ['webshop' => $webshop]); } public function callTestMethod() { // 期望返回Mock的空stdClass对象 dd($this->client->shipments); } }
对应的服务提供者:
<?php namespace App\Providers; use App\Webshop; use Illuminate\Support\ServiceProvider; class WebshopappApiClientServiceProvider extends ServiceProvider { public function register() { $this->app->bind(\WebshopappApiClient::class, function ($app, $params): \WebshopappApiClient { /** @var Webshop $webshop */ $webshop = $params['webshop']; $key = $webshop->key; $value = $webshop->value; return new \WebshopappApiClient($key, $value); }); } }
尝试用Mockery模拟$this->client->shipments的调用,测试代码如下:
public function testShipWithTracking(): void { $mockClient = Mockery::mock(\WebshopappApiClient::class); $mockClient->shipments = Mockery::mock('stdClass'); app()->instance(\WebshopappApiClient::class, $mockClient); $webshop = new Webshop([ 'key' => 'test', 'value' => 'test', ]); $testClass = new Testclass($webshop); // 实际仍返回真实对象而非Mock的空stdClass $testClass->callTestMethod(); }
问题:模拟未生效,如何在不修改Testclass构造函数的前提下,确保服务绑定被Mock对象覆盖?核心目标是模拟该API客户端类的所有API响应,且该类在遗留代码中被大量调用。
解决方案
问题根源
app()->instance()注册的共享实例,仅在不带参数调用app()->make()时生效。而Testclass中调用app()->make(WebshopappApiClient::class, ['webshop' => $webshop])时,Laravel会优先使用服务提供者中注册的带参数绑定回调,忽略instance注册的实例,导致Mock失效。
解决方法:重新绑定带参数的服务回调
直接替换服务容器中WebshopappApiClient::class的绑定逻辑,让它无论传入什么参数都返回Mock实例:
public function testShipWithTracking(): void { // 创建Mock的shipments对象 $mockShipments = Mockery::mock(stdClass::class); // 创建Mock的API客户端,并赋值shipments属性 $mockClient = Mockery::mock(\WebshopappApiClient::class); $mockClient->shipments = $mockShipments; // 重新绑定服务,覆盖原有的带参数回调 $this->app->bind(\WebshopappApiClient::class, function ($app, $params) use ($mockClient) { return $mockClient; }); $webshop = new Webshop([ 'key' => 'test', 'value' => 'test', ]); $testClass = new Testclass($webshop); $testClass->callTestMethod(); // 现在会返回Mock的shipments对象 }
另一种简化写法(利用Laravel测试辅助函数)
如果使用Laravel的测试框架,可以直接用$this->mock()方法,同时处理构造参数的兼容:
public function testShipWithTracking(): void { $mockShipments = Mockery::mock(stdClass::class); // Mock客户端类,指定解析时忽略构造参数,返回Mock实例 $this->mock(\WebshopappApiClient::class, function ($mock) use ($mockShipments) { $mock->shipments = $mockShipments; // 允许任意构造参数调用 $mock->shouldReceive('__construct')->withAnyArgs()->andReturnSelf(); }); $webshop = new Webshop([ 'key' => 'test', 'value' => 'test', ]); $testClass = new Testclass($webshop); $testClass->callTestMethod(); }
注意事项
- 测试结束后需清理Mockery,避免影响其他测试:可以在测试类的
tearDown方法中添加Mockery::close();,或使用Laravel内置的Mockery集成(Laravel 8+默认支持)。 - 如果需要模拟特定方法的返回值,可在Mock对象上添加
shouldReceive断言,例如:$mockShipments->shouldReceive('create')->withAnyArgs()->andReturn(true);
内容的提问来源于stack exchange,提问作者chishiki
相关产品推荐
相关产品推荐

