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

