在Laravel单元测试场景中,我们经常会遇到这样的情况:需要测试某个服务类的逻辑,这个服务类依赖某个模型的部分方法,但又不想完全模拟整个模型,否则会丢失模型其他真实方法的验证机会,这时候就需要用到模型部分模拟的方法。

为什么需要模型部分模拟
完全模拟模型虽然能隔离依赖,但也会带来两个问题:一是如果模型本身有一些基础逻辑需要验证,完全模拟会跳过这些逻辑;二是当服务类调用的模型方法较多时,完全模拟需要逐个定义所有方法的返回值,增加了测试代码的冗余度。部分模拟则可以在保留模型真实逻辑的基础上,只替换需要模拟的方法,让测试更贴近真实运行场景。
准备工作
首先确保你的Laravel项目已经安装了PHPUnit和Mockery,Laravel默认已经集成了这两个工具,无需额外安装。我们需要测试的服务类示例如下:
<?php
namespace AppServices;
use AppModelsUser;
class UserService
{
public function updateUserScore(int $userId, int $addScore): array
{
$user = User::find($userId);
if (!$user) {
return ['success' => false, 'msg' => '用户不存在'];
}
// 调用需要模拟的方法
$result = $user->calculateFinalScore($addScore);
$user->score = $result['score'];
$user->save();
return ['success' => true, 'msg' => '更新成功', 'data' => $user];
}
}
这个服务类中,User::find和$user->save我们希望走真实逻辑,而calculateFinalScore方法我们希望模拟返回值,避免依赖外部计算逻辑。
正确的部分模拟实现步骤
第一步:创建部分模拟对象
使用Mockery的mock方法时,传入模型的完整类名,然后通过shouldReceive只定义需要模拟的方法,其他方法会保留真实逻辑。注意不要使用shouldIgnoreMissing,除非你明确知道要忽略哪些方法。
第二步:替换容器中的模型绑定
为了让服务类中调用的User::find返回我们模拟的部分对象,需要将模拟对象绑定到Laravel的服务容器中,替换原有的模型绑定。
完整测试代码示例
<?php
namespace TestsUnitServices;
use AppModelsUser;
use AppServicesUserService;
use TestsTestCase;
use Mockery;
class UserServiceTest extends TestCase
{
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
public function testUpdateUserScore()
{
// 创建用户真实实例,用于模拟find返回
$realUser = new User([
'id' => 1,
'name' => '测试用户',
'score' => 100
]);
// 部分模拟User模型,只模拟calculateFinalScore方法
$mockUser = Mockery::mock(User::class)->makePartial();
// 定义需要模拟的方法返回值
$mockUser->shouldReceive('calculateFinalScore')
->once()
->with(50)
->andReturn(['score' => 150]);
// 绑定模拟对象到容器,让User::find返回模拟对象
$this->app->instance(User::class, $mockUser);
// 模拟User::find的返回值,这里可以结合真实逻辑或者手动返回
// 因为makePartial会保留真实方法,所以也可以直接让find返回模拟对象
// 这里为了更清晰,手动设置find的返回
User::shouldReceive('find')->with(1)->andReturn($mockUser);
$service = new UserService();
$result = $service->updateUserScore(1, 50);
$this->assertTrue($result['success']);
$this->assertEquals(150, $result['data']['score']);
}
}
常见错误规避
- 不要对模型使用完全模拟(
Mockery::mock(User::class)不调用makePartial),否则save等方法会没有实现,导致测试报错。 - 部分模拟后,如果调用了没有定义模拟也没有真实实现的方法,会抛出错误,需要确认所有调用的方法要么有模拟定义,要么有真实实现。
- 测试结束后一定要调用
Mockery::close(),避免模拟对象残留影响其他测试用例。
总结
Laravel中实现模型部分模拟的核心是结合Mockery的makePartial方法,只替换需要模拟的方法,同时保留模型的其他真实逻辑,再通过容器绑定让服务类调用到模拟对象。这种方式既能隔离不需要的依赖,又能保证测试覆盖到模型的真实基础逻辑,是编写高效单元测试的重要技巧。