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

Laravel单元测试中Mock受保护方法未返回预设值的问题解决

问题描述

我有一个名为ModuleManifest的类,构造函数代码如下:

/**
 * Create a new instance of the ModuleManifest class.
 *
 * @param string $vendor The vendor of the module.
 * @param string $package The name of the package.
 * @return void
 */
public function __construct(string $vendor, string $package)
{
    $manifest = $this->manifestPath($vendor, $package);

    if (!File::exists($manifest))
    {
        throw new \InvalidArgumentException(sprintf(
            'Package %s from vendor %s does not contain a manifest',
            $package, $vendor
        ));
    }

    $this->manifest = $this->load($manifest);
}

该类的load和manifestPath方法定义如下:

/**
 * Load a module manifest into an object.
 *
 * @param string $path The path to the module.
 * @return object
 */
protected function load(string $path) : object
{
    return json_decode(file_get_contents($path));
}

/**
 * Determine the path to the module manifest.
 *
 * @param string $vendor The vendor of the module.
 * @param string $package The name of the package.
 * @return string
 */
protected function manifestPath(string $vendor, string $package) : string
{
    return implode('/', [base_path('vendor'), $vendor, $package, 'resources/manifest.json']);
}

我编写了单元测试验证该类是否返回对象:

/** @testdox Returns an object when the path exists */
public function test_manifest_returns_object() : void
{
    File::shouldReceive('exists')->once()->andReturnTrue();

    $this->mock(ModuleManifest::class, function (MockInterface $mock) {
        $mock->shouldAllowMockingProtectedMethods();
        $mock->shouldReceive('load')->once()->andReturn(
            json_decode('{"name": "testModule"}')
        );
    });

    $manifest = new ModuleManifest('vendor', 'package');

    $this->assertTrue($manifest->has('name'));
}

测试时抛出错误:

ErrorException: file_get_contents(...): Failed to open stream: No such file or directory

我期望Mock的load方法返回预设对象,但测试仍执行了真实的load方法,尝试过部分Mock也没用,该怎么解决?

解决方法

问题核心是你之前用$this->mock()创建的是完整Mock,但构造函数在实例化时直接执行,此时Mock还未完全生效,导致真实load方法被调用。可以用以下两种方式解决:

方式一:用部分Mock指定构造参数

直接创建部分Mock,保留构造函数逻辑,同时替换load方法的实现:

/** @testdox Returns an object when the path exists */
public function test_manifest_returns_object() : void
{
    File::shouldReceive('exists')->once()->andReturnTrue();

    // 创建部分Mock,传入构造参数,替换load方法
    $manifest = $this->partialMock(ModuleManifest::class, ['vendor', 'package'], function (MockInterface $mock) {
        $mock->shouldAllowMockingProtectedMethods();
        $mock->shouldReceive('load')->once()->andReturn(
            json_decode('{"name": "testModule"}')
        );
    });

    $this->assertTrue($manifest->has('name'));
}

这里partialMock的第二个参数是构造函数的参数,实例化时会直接使用带Mock逻辑的对象,确保load方法被替换。

方式二:Mock manifestPath返回虚拟路径

如果不想处理load方法的Mock,也可以MockmanifestPath返回一个存在的测试用JSON文件路径,或者直接Mockfile_get_contents函数:

/** @testdox Returns an object when the path exists */
public function test_manifest_returns_object() : void
{
    // Mock manifestPath返回自定义测试文件路径
    $this->partialMock(ModuleManifest::class, ['vendor', 'package'], function (MockInterface $mock) {
        $mock->shouldAllowMockingProtectedMethods();
        $mock->shouldReceive('manifestPath')->once()->andReturn(__DIR__.'/test-manifest.json');
    });

    // 或者直接Mock file_get_contents函数返回预设内容
    // $this->mockFunction('file_get_contents', function () {
    //     return '{"name": "testModule"}';
    // });

    $manifest = app(ModuleManifest::class);
    $this->assertTrue($manifest->has('name'));
}

这种方式更贴近真实场景,但需要确保测试用JSON文件存在,或者直接Mockfile_get_contents函数。

关键说明

你之前的写法中,$this->mock()创建的Mock对象,在执行new ModuleManifest()时其实是生成了一个新的真实实例,而非Mock实例。而partialMock会直接创建带Mock逻辑的实例,并且传入构造参数,确保构造函数执行时使用的是Mock后的方法。

内容的提问来源于stack exchange,提问作者madman2819

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 19:39:57