直接 new Service() 在测试中失败,因绕过 Laravel 服务容器,导致无法被 Mockery 替换,进而调用真实外部服务引发超时、数据污染等问题;必须通过容器(构造注入或 app())获取依赖,并用 instance 绑定或 shouldReceive 拦截来 mock。

为什么直接 new Service() 在测试里会失败
测试中调用真实外部服务(比如 HttpService、PaymentGateway)会导致:请求超时、数据污染、CI 环境无网络、响应不可控。Laravel 的服务容器默认每次解析都返回新实例,不会自动替换成 mock —— 你得手动绑定。
- 别在测试里写
new HttpService(),它绕过容器,Mockery 拦不住 - 确保被测类是通过构造函数或
app()从容器获取依赖的 - 若服务注册在
AppServiceProvider中用了 singleton,mock 后需手动$this->app->forgetInstance()
用 Mockery 替换容器中的具体类(推荐方式)
最稳妥的做法是用 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 需确认)
对 Facade 或静态调用怎么 mock?用 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'),比断言更早暴露逻辑错误 - 注意:Laravel 9+ 默认禁用 Facade mock(因
Mockery不再自动 patch static calls),需在phpunit.xml中保留processIsolation="false",且不要启用statically()以外的隔离模式
HTTP 客户端 mock(Http facade / GuzzleHttp\Client)
Laravel 的 Http facade 底层用的是 Guzzle,但 mock 策略分两层:Facade 层用 shouldReceive(),Guzzle 实例层用 HandlerStack 或 MockHandler。
- 简单场景优先 mock
Httpfacade:Http::shouldReceive('post')->andReturn(HttpResponse::fake()) - 需要精确控制响应头、状态码、重试行为时,应 mock Guzzle 的
HandlerStack,并在测试前注入:$this->app->bind(Client::class, function () { return new Client(['handler' => HandlerStack::create(new MockHandler([...]))]); }); - 避免混合使用:不要一边 mock
Http::post(),一边又在被测代码里直接 newClient(),那 mock 就失效了
app()?),就从那里下手。容器绑定和 Facade 拦截这两条路径覆盖了 95% 的场景;剩下那些绕过容器的手动 new,得先重构代码,再谈 mock。










