Laravel中使用PHPUnit模拟命令类单个方法失败如何解决?
问题描述
我正在测试一个Laravel命令,其中的methodToMock方法通过Guzzle调用外部服务,我希望模拟该方法,但无论尝试何种方式,始终会调用原方法。相关代码如下:
命令类代码
class CommandToTest extends Command { //constants, setup handle and other stuff //other methods protected function methodToMock() { //method with the call to external web service that should be mocked } //other methods }
测试代码
class TestForCommand extends TestCase { //constants etc public function testCommandToTest() { $commandMock = Mockery::mock(CommandToTest::class)->makePartial(); $commandMock->shouldAllowMockingProtectedMethods()->shouldReceive('methodToMock') ->andReturn([ 'Test01' => 'T01', 'Test02' => 'T02', 'Test03' => 'T03' ]); $this->app->instance(CommandToTest::class, $commandMock); //other stuff $this->artisan('tools:commandtotest', [//params]) ->expectsConfirmation(//params ok) ->assertOk(); } //assertions }
请问该如何让模拟生效?是否有其他实现思路?
解决方案
让当前模拟生效的调整
问题核心是Laravel的artisan()方法不会直接从容器中解析已绑定的Mock实例,而是通过命令工厂重新构建对象。你可以通过以下两种方式修正:
直接执行Mock实例
跳过artisan()方法,手动配置Mock实例的输入参数后直接执行:public function testCommandToTest() { $commandMock = Mockery::mock(CommandToTest::class)->makePartial(); $commandMock->shouldAllowMockingProtectedMethods() ->shouldReceive('methodToMock') ->andReturn([ 'Test01' => 'T01', 'Test02' => 'T02', 'Test03' => 'T03' ]); // 绑定Laravel应用实例 $commandMock->setLaravel($this->app); // 设置命令输入参数 $commandMock->setInput(new ArrayInput([ // 你的命令参数,比如 '--option' => 'value' ])); // 执行命令并断言退出码 $exitCode = $commandMock->handle(); $this->assertEquals(0, $exitCode); // 其他自定义断言逻辑 }使用容器
swap()方法替换实例
通过swap()强制让容器返回你的Mock实例,再手动解析执行:public function testCommandToTest() { $commandMock = Mockery::mock(CommandToTest::class)->makePartial(); $commandMock->shouldAllowMockingProtectedMethods() ->shouldReceive('methodToMock') ->andReturn([ 'Test01' => 'T01', 'Test02' => 'T02', 'Test03' => 'T03' ]); // 替换容器中的命令实例 $this->app->swap(CommandToTest::class, $commandMock); // 从容器解析并执行命令 $command = $this->app->make(CommandToTest::class); $exitCode = $command->handle(); $this->assertEquals(0, $exitCode); }
更优的替代思路
直接模拟命令类的方法并非最佳实践,更符合SOLID原则的做法是抽离外部服务逻辑为独立依赖类,通过依赖注入解耦,测试时模拟这个依赖:
抽离外部服务逻辑
创建专门的客户端类封装Guzzle调用:class ExternalServiceClient { public function fetchData() { // 原methodToMock中的Guzzle调用逻辑 } }修改命令类注入依赖
在命令的构造函数中注入外部服务客户端:class CommandToTest extends Command { protected $externalService; public function __construct(ExternalServiceClient $externalService) { parent::__construct(); $this->externalService = $externalService; } protected function methodToMock() { return $this->externalService->fetchData(); } // 其他方法 }测试时模拟依赖类
直接模拟外部服务客户端,通过容器绑定后正常使用artisan()方法:public function testCommandToTest() { // 模拟外部服务客户端 $serviceMock = Mockery::mock(ExternalServiceClient::class); $serviceMock->shouldReceive('fetchData') ->andReturn([ 'Test01' => 'T01', 'Test02' => 'T02', 'Test03' => 'T03' ]); // 绑定模拟实例到容器 $this->app->instance(ExternalServiceClient::class, $serviceMock); // 正常执行命令并断言 $this->artisan('tools:commandtotest', [/* 参数 */]) ->expectsConfirmation(/* 确认项 */) ->assertOk(); // 其他自定义断言 }
这种方式不仅让测试更灵活,还提升了代码的可维护性,符合依赖倒置原则。
内容的提问来源于stack exchange,提问作者Miguel Heredia
相关产品推荐
相关产品推荐

