Laravel 单元测试中实现模型部分模拟的正确方法

3次阅读

Laravel 单元测试中实现模型部分模拟的正确方法

laravel 单元测试中,直接对 eloquent 模型进行部分模拟(partial mock)时,若仍调用真实方法,通常是因为未正确使用 mockery 的 `makepartial()`,或错误地通过 `new order()` 实例化对象而非获取容器中已绑定的模拟实例。

要成功实现模型的部分模拟(即仅拦截特定方法如 save(),其余方法保留原始逻辑),关键在于两点:正确创建部分模拟对象,以及确保测试代码实际使用该模拟实例,而非重新 new 一个真实模型。

首先,Mockery::mock(“AppOrder[save]”) 的语法虽曾用于旧版 Mockery,但在当前主流版本(如 Mockery 1.x + Laravel 9/10)中已不推荐;更可靠、语义更清晰的方式是显式调用 ->makePartial():

$mock = Mockery::mock(Order::class)->makePartial();

其次,$this->app->instance(“appOrder”, $mock) 仅将模拟对象绑定到服务容器中,并不会自动影响 new Order() 的行为——因为 new 是 php 原生实例化,完全绕过 laravel 容器。因此,原测试中 $order = new Order() 创建的是真实类实例,其 save() 方法自然不会被拦截。

✅ 正确做法是:直接操作 $mock 实例,或通过容器解析获取(如 app(Order::class)),从而确保使用的是已配置好的部分模拟对象。

以下是推荐的完整写法(兼容 Laravel 9+ 和 Mockery 1.6+):

命名空间路径(Laravel 8+ 默认在 Models 下)  class MockTest extends TestCase {     protected function tearDown(): void     {         Mockery::close(); // 确保清理 Mockery 资源         parent::tearDown();     }      /** @test */     public function it_can_partially_mock_order_save_method()     {         // 创建 Order 类的部分模拟:仅重写 save(),其他方法保持原逻辑         $mock = Mockery::mock(Order::class)->makePartial();          // 预期 save() 被调用一次,返回 5         $mock->shouldReceive('save')->once()->andReturn(5);          // 将模拟实例绑定至容器,供后续依赖注入使用(可选,本例中直接调用 mock 即可)         $this->app->instance(Order::class, $mock);          // ✅ 正确:直接调用 mock 实例的方法         $result = $mock->save();          $this->assertEquals(5, $result);     } }

⚠️ 注意事项:

  • 避免 new Order():它永远创建真实实例,与容器绑定无关;
  • makePartial() 是核心:它使模拟对象继承原类所有方法,仅对 shouldReceive() 声明的方法进行拦截;
  • 及时清理 Mockery:在 tearDown() 中调用 Mockery::close(),防止模拟对象泄漏影响其他测试;
  • 命名空间一致性:确认 Order 类的实际命名空间(如 AppModelsOrder),并在 mock() 中准确传入;
  • 替代方案考虑:对于简单场景,也可使用 Laravel 内置的 Mockery::spy() 或更现代的「测试替身(test double)」策略(如用内存数据库 + DatabaseTransactions),但部分模拟适用于需隔离副作用(如网络请求、文件写入)的 save() 扩展逻辑。

总结:Laravel 模型的部分模拟失败,根源常在于混淆了「容器绑定」与「对象实例化」机制。牢记——绑定是为了让依赖注入生效,而测试中若需控制行为,应直接操作模拟对象本身。

text=ZqhQzanResources