如何在Laravel测试中模拟(Mock)外部服务? (Mockery使用技巧)

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

如何在laravel测试中模拟(mock)外部服务? (mockery使用技巧)

为什么直接 new Service() 在测试里会失败

测试中调用真实外部服务(比如 HttpServicePaymentGateway)会导致:请求超时、数据污染、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() 拦截静态方法调用,常见于 CacheStorage、自定义 Facade。

Ideogram

Ideogram

Ideogram是一个全新的文本转图像AI绘画生成平台,擅长于生成带有文本的图像,如LOGO上的字母、数字等。

下载

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 实例层用 HandlerStackMockHandler

  • 简单场景优先 mock Http facade: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(),一边又在被测代码里直接 new Client(),那 mock 就失效了

Mockery 的关键不是“写得多”,而是“替换得准”——盯住依赖是怎么进来的(构造注入?Facade?app()?),就从那里下手。容器绑定和 Facade 拦截这两条路径覆盖了 95% 的场景;剩下那些绕过容器的手动 new,得先重构代码,再谈 mock。

https://www.php.cn/faq/1967966.html

发表回复

Your email address will not be published. Required fields are marked *