如何用PHPUnit为类依赖正确创建Mock?无法修改目标服务类
问题描述
我有一个TypeSaveLogService服务类,其中直接创建了TypeSaveLogRecord对象,代码如下:
class TypeSaveLogService { public function store(string $type, string $smId): TypeSaveLogRecord { $log = new TypeSaveLogRecord([ 'mdm_id' => $smId, 'type' => $type ]); $saveResult = $log->save(); if (!$saveResult) { throw new \Exception('Failed to save type log'); } } }
我想要为TypeSaveLogService编写测试用例来测试store方法的功能,我的测试代码如下:
public function testStoreFailed() { $Mock = $this->createMock(TypeSaveLogRecord::class); $Mock->method('save')->willReturn(false); $service = new TypeSaveLogService(); $actualRes = $service->store($type, $smId); $this->expectException('Exception'); }
但这段测试代码无法生效,我无法通过这种方式为TypeSaveLogRecord创建Mock。请问我哪里出错了?遗憾的是我无法修改TypeSaveLogService的代码。
问题分析与解决方案
错误原因
- Mock实例未被服务类使用:你创建的Mock是独立实例,但
TypeSaveLogService::store内部是直接new TypeSaveLogRecord()生成新对象,和你的Mock完全无关,所以Mock的save返回值根本不会生效。 - 预期异常声明位置错误:
$this->expectException()必须放在调用可能抛出异常的方法之前,否则store抛出异常时测试会直接终止,无法检测到预期的异常。
解决方案(无需修改服务类)
因为不能修改原服务类,无法通过依赖注入传入Mock,所以需要用类重载的方式,让服务类内部new出来的TypeSaveLogRecord变成我们的Mock。以下是两种可行实现:
方案1:基于PHPUnit原生实现
use PHPUnit\Framework\TestCase; class TypeSaveLogServiceTest extends TestCase { public function testStoreFailed() { // 创建TypeSaveLogRecord的Mock,指定要mock的方法 $mockLogRecord = $this->getMockBuilder(TypeSaveLogRecord::class) ->disableOriginalConstructor() ->setMethods(['save']) ->getMock(); // 让Mock的save方法返回false,触发异常 $mockLogRecord->method('save')->willReturn(false); // 让new TypeSaveLogRecord时返回我们的Mock $this->getMockBuilder(TypeSaveLogRecord::class) ->setMockClassName(TypeSaveLogRecord::class) ->disableOriginalConstructor() ->setMethods(['save']) ->getMock(); // 提前声明预期的异常类型和消息 $this->expectException(\Exception::class); $this->expectExceptionMessage('Failed to save type log'); // 调用服务方法 $service = new TypeSaveLogService(); $service->store('test_type', 'test_sm_id'); } }
方案2:基于Mockery实现(更简洁)
use Mockery; use PHPUnit\Framework\TestCase; class TypeSaveLogServiceTest extends TestCase { protected function tearDown(): void { Mockery::close(); } public function testStoreFailed() { // 重载TypeSaveLogRecord类,当new时返回Mock实例 $mockLogRecord = Mockery::mock('overload:' . TypeSaveLogRecord::class); $mockLogRecord->shouldReceive('save')->once()->andReturn(false); // 声明预期异常 $this->expectException(\Exception::class); $this->expectExceptionMessage('Failed to save type log'); // 调用服务方法 $service = new TypeSaveLogService(); $service->store('test_type', 'test_sm_id'); } }
关键说明
类重载的核心是让PHP在实例化原类时,返回预先创建的Mock实例,这样服务类内部的new操作就会使用Mock而非真实类。同时务必保证expectException在调用store方法之前声明,这样PHPUnit才能正确捕获预期异常。
内容的提问来源于stack exchange,提问作者Neodrive
相关产品推荐
相关产品推荐

