Facade::shouldReceive() 是 Laravel 测试中 Mock 门面(Facade) 的核心方法,其底层并未修改服务容器(Service Container),而是动态替换门面的 accessor(访问器),将调用重定向到 Mockery 对象。


一、核心机制:门面 accessor 替换

1. 门面工作原理回顾

  • 门面(如 Cache::get())通过 getFacadeAccessor() 获取服务名(如 'cache'
  • 调用时委托给 服务容器
    // Illuminate\Support\Facades\Facade
    protected static function resolveFacadeInstance($name)
    {
        return static::$resolvedInstance[$name] ?? app($name);
    }
    

2. shouldReceive() 的关键操作

  • 步骤 1:调用 Mockery::mock() 创建 Mock 对象
  • 步骤 2将 Mock 对象存入 Facade::$resolvedInstance
    // Illuminate\Support\Facades\Facade
    public static function shouldReceive()
    {
        $mock = Mockery::mock(...);
        static::$resolvedInstance[static::getFacadeAccessor()] = $mock;
        return $mock;
    }
    
  • 结果:后续门面调用直接使用 Mock 对象绕过服务容器

本质
shouldReceive() 是门面层的 Mock,非容器层的绑定替换


二、与服务容器的关系

1. 服务容器未被修改

  • 验证
    // 测试中
    Cache::shouldReceive('get')->andReturn('mocked');
    
    // 服务容器仍返回真实实例
    $realCache = app('cache'); // 不是 Mockery 对象!
    
  • 原因
    Facade::$resolvedInstance门面内部的静态缓存,与容器 app()->make() 无关

2. 门面调用路径变更

graph LR
A[Cache::get('key')] --> B{Facade::$resolvedInstance['cache'] exists?}
B -->|Yes| C[Call Mockery Object]
B -->|No| D[app('cache') → Real Instance]

三、典型使用场景

1. Mock 外部服务

// 测试支付流程
public function test_payment_success()
{
    // Mock 支付网关门面
    PaymentGateway::shouldReceive('charge')
        ->once()
        ->with(100, 'user_token')
        ->andReturn(true);
    
    $result = $this->post('/checkout', ['amount' => 100]);
    $this->assertTrue($result['success']);
}

2. 避免副作用

// Mock 邮件发送
Mail::shouldReceive('to')->andReturnSelf();
Mail::shouldReceive('send')->once();
User::register('test@example.com'); // 不会真发邮件

四、重要限制与陷阱

1. 仅对门面有效

  • 不适用于
    • 直接依赖注入(public function __construct(Cache $cache)
    • 容器解析(app(Cache::class)
  • 解决方案:用 $this->mock()(Laravel 8+):
    $mock = $this->mock(Cache::class, function ($mock) {
        $mock->shouldReceive('get')->andReturn('mocked');
    });
    

2. 静态属性作用域

  • 每个门面类独立
    Cache::shouldReceive('get'); // 只影响 Cache 门面
    DB::shouldReceive('table');  // 不影响 DB 门面
    

3. 测试后需清理

  • Laravel 自动清理
    CreatesApplication trait 在 tearDown 中重置 Facade::$resolvedInstance
  • 手动清理(如自定义测试基类):
    protected function tearDown(): void
    {
        Facade::clearResolvedInstances();
        parent::tearDown();
    }
    

五、与 $this->mock() 的对比(Laravel 8+)

方法 作用层 适用对象 底层机制
Facade::shouldReceive() 门面层 门面(Cache/Mail 替换 Facade::$resolvedInstance
$this->mock() 容器层 任何类/接口 绑定 Mockery 对象到容器

示例对比

// 门面 Mock(仅门面调用生效)
Cache::shouldReceive('get')->andReturn('mocked');

// 容器 Mock(所有解析方式生效)
$this->mock(Cache::class, function ($mock) {
    $mock->shouldReceive('get')->andReturn('mocked');
});

原则

  • 用门面? → shouldReceive()
  • 用依赖注入? → $this->mock()

六、源码关键片段(Laravel 10)

Facade::shouldReceive()

// Illuminate\Support\Facades\Facade
public static function shouldReceive()
{
    $name = static::getFacadeAccessor();
    
    // 1. 创建 Mockery Mock
    $mock = Mockery::mock(...func_get_args());
    
    // 2. 存入门面静态缓存(绕过容器)
    static::$resolvedInstance[$name] = $mock;
    
    return $mock;
}

Facade::clearResolvedInstances()

// 测试后清理
public static function clearResolvedInstances()
{
    static::$resolvedInstance = [];
}

七、总结

问题 答案
shouldReceive() 修改容器绑定吗 ,仅替换门面内部缓存
底层机制 Facade::$resolvedInstance 存储 Mockery 对象
适用场景 Mock 门面调用(如 Cache/Mail
如何 Mock 依赖注入 $this->mock()(Laravel 8+)

核心原则
门面 Mock 是“快捷方式”,容器 Mock 是“根本解法”
理解二者差异,
才能写出精准、可靠的 Laravel 测试。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐