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

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原则的做法是抽离外部服务逻辑为独立依赖类,通过依赖注入解耦,测试时模拟这个依赖:

  1. 抽离外部服务逻辑
    创建专门的客户端类封装Guzzle调用:

    class ExternalServiceClient
    {
        public function fetchData()
        {
            // 原methodToMock中的Guzzle调用逻辑
        }
    }
    
  2. 修改命令类注入依赖
    在命令的构造函数中注入外部服务客户端:

    class CommandToTest extends Command
    {
        protected $externalService;
    
        public function __construct(ExternalServiceClient $externalService)
        {
            parent::__construct();
            $this->externalService = $externalService;
        }
    
        protected function methodToMock()
        {
            return $this->externalService->fetchData();
        }
    
        // 其他方法
    }
    
  3. 测试时模拟依赖类
    直接模拟外部服务客户端,通过容器绑定后正常使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 18:44:59