直接 new Service() 在测试中失败,因绕过 Laravel 服务容器,导致无法被 Mockery 替换,进而调用真实外部服务引发超时、数据污染等问题;必须通过容器(构造注入或 app())获取依赖,并用 instance 绑定或 shouldReceive 拦截来 mock。
测试中调用真实外部服务(比如 HttpService、PaymentGateway)会导致:请求超时、数据污染、CI 环境无网络、响应不可控。Laravel 的服务容器默认每次解析都返回新实例,不会自动替换成 mock —— 你得手动绑定。
new HttpService(),它绕过容器,Mockery 拦不住app() 从容器获取依赖的AppServiceProvider 中用了 singleton,mock 后需手动 $this->app->forgetInstance()
最稳妥的做法是用 instance 绑定覆盖容器中的类绑定。适用于 Laravel 8+ 和大多数自定义服务。
public function test_payment_fails_gracefully()
{
$mock = Mockery::mock(PaymentGateway::class);
$mock->expects('charge')->andThrows(new ConnectionException('timeout'));
$this->app->instance(PaymentGateway::class, $mock);
$response = $this->postJson('/api/charge', ['amount' => 100]);
$response->assertStatus(500);
}
$this->app->instance() 之前调用 Mockery::mock(),否则容器仍返回原始实例bind() 或 singleton() 里声明),先补
$this->app->bind(PaymentGateway::class, PaymentGateway::class)
$this->tearDownMockery()(Laravel TestCase 已内置,但自定义 TestCase 需确认)shouldReceive() + shouldNotReceive()
Facade 本质是静态代理,不能用 instance。得用 shouldReceive() 拦截静态方法调用,常见于 Cache、Storage、自定义 Facade。
public function test_cache_is_used_for_user_profile()
{
Cache::shouldReceive('get')
->with('user:123:profile')
->andReturn(['name' => 'Alice']);
$profile = app(UserProfileService::class)->get(123);
$this->assertEquals('Alice', $profile['name']);
}
Cache::shouldReceive('get') 会拦截所有后续对 Cache::get() 的调用,包括在被测代码内部发生的shouldNotReceive('delete'),比断言更早暴露逻辑错误Mockery 不再自动 patch static calls),需在 phpunit.xml 中保留 processIsolation="false",且不要启用 statically() 以外的隔离模式Http facade / GuzzleHttp\Client)Laravel 的 Http facade 底层用的是 Guzzle,但 mock 策略分两层:Facade 层用 shouldReceive(),Guzzle 实例层用 HandlerStack 或 MockHandler。
Http facade:Http::shouldReceive('post')->andReturn(HttpResponse::fake())
HandlerStack,并在测试前注入:$this->app->bind(Client::class, function () { return new Client(['handler' => HandlerStack::create(new MockHandler([...]))]); });
Http::post(),一边又在被测代码里直接 new Client(),那 mock 就失效了app()?),就从那里下手。容器绑定和 Facade 拦截这两条路径覆盖了 95% 的场景;剩下那些绕过容器的手动 new,得先重构代码,再谈 mock。