You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 10:03:27