【问题标题】:How can I mock a class that is used in an artisan command for testing?如何模拟在工匠命令中用于测试的类?
【发布时间】:2019-03-14 09:32:06
【问题描述】:

我有一个调用另一个类函数的工匠命令。此函数向另一台服务器发出 get 请求,我不希望在测试期间发生此 get 请求。

我通常的解决方案是使用 mockery 来模拟该函数,但这似乎不起作用。

为什么当我使用 Artisan::call('command::getFoo') 调用 artisan 命令时没有调用我的 mock?

命令类

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Foo;

class GetFoo extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:getFoo';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Get the foo data';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {       
        return Foo::get();  // Returns true
    }
}

测试类

namespace Tests\Feature;

use Tests\TestCase;
use App\Foo;

class FooCommandTest extends TestCase 
{
    public function testThatWeCanGetFoo()
    {
        $fooClass = Mockery::mock(Foo::class);
        $fooClass->shouldReceive(['get' => false]); // Overwrite the foo class to return false instead of true
        $fooData = \Artisan::call('command:getFoo');

        $this->assertFalse($fooData);
    }
}

当我运行测试时它失败了,因为它仍然恢复真实。这意味着没有调用嘲笑类。这里发生了什么?如何测试这个命令?

【问题讨论】:

  • Foo 类静态引用你的真实世界依赖,这就是它不使用模拟对象的原因。您需要将其作为依赖项注入到您的工匠命令中。老实说,不确定如何在命令上实现这一点,但可以肯定的是,快速的谷歌可能会显示出方法。
  • 好的,谢谢。这让我找到了解决方案。
  • 很高兴知道,请随时发布答案,以便其他人学习。我也很好奇。问候!

标签: php laravel phpunit mockery


【解决方案1】:

您可以尝试创建 Artisan Facade 使用的根类的 dummy-child,并覆盖任何执行不需要的操作的方法。然后,在你的测试中,调用Artisan::swap($dummyObj); 来替换它。

--

它没有很好的记录,我想我是在寻找一种细粒度的方法来允许在我的测试中触发某些事件并禁止其他事件以避免附带操作时遇到的。所以Illuminate\Support\Facades\Event::fake() 是一个很好的例子来说明如何使用它。

所以看看引擎盖下:

  • config/app.phpArtisan 定义为指向Illuminate\Support\Facades\Artisan
  • Illuminate\Support\Facades\Artisan::getFacadeAccessor() 将外观访问器的名称定义为Illuminate\Contracts\Console\Kernel 接口名称。
  • 并且 Kernel 接口绑定到 bootstrap/app.php 中的 App\Console\Kernel 单例。

总计,这意味着任何时候您调用\Artisan,您实际上是在与App\Console\Kernel 的可重用实例交谈。

App\Console\Kernel 扩展 Illuminate\Foundation\Console\Kernel,而 that 就是拥有您想要控制其行为的 call() 命令的东西。

那么你想要的是这样的:

namespace Tests\Dummies;

use App\Console\Kernel as BaseKernel;

class Artisan extends BaseKernel
{
    // This will override the parent's call() and block it from doing anything.
    public function call($command, array $parameters = [], $outputBuffer = null)
    {
        // -- Do nothing instead, or add some debug logging here --
    }
}

然后,作为测试中的第一个操作或setUp() 方法的一部分,请执行以下操作:

$dummyArtisan = app(Tests\Dummies\Artisan::class);
Artisan::swap($dummyArtisan);

请注意,所有这些都未经测试。我过去做过类似的事情,可以肯定地说这个理论是可靠的。但是不知道这个具体的代码是不是。

--

您可能还想查看Illuminate\Support\Facades\Facade::spy()Illuminate\Support\Facades\Facade::shouldReceive()。我只是在查看此处编写此答案的代码时发现了他们,他们现在让我想知道我的提议是否会重新发明轮子。看起来 Facades 的设计有点意识到它们很难模拟,所以他们有一些工具可以做到这一点。

【讨论】:

  • 看起来很有趣,你能扩展你的解释吗?
  • 当然,我编辑了我的答案以包含一些示例用法。
猜你喜欢
  • 2019-08-08
  • 2021-01-29
  • 2015-06-24
  • 1970-01-01
  • 2015-07-09
  • 2023-04-05
  • 2019-06-14
  • 1970-01-01
  • 2021-09-27
相关资源
最近更新 更多