如何为调用外部 API 的 Laravel Artisan 命令编写 PHPUnit 测试,而无需物理调用该 API?

Posted

技术标签:

【中文标题】如何为调用外部 API 的 Laravel Artisan 命令编写 PHPUnit 测试,而无需物理调用该 API?【英文标题】:How to write a PHPUnit test for Laravel Artisan command that calls an external API without calling that API physically? 【发布时间】:2018-12-11 08:20:59 【问题描述】:

简要介绍,让您了解我想要实现的目标......

我有一个 Laravel 5.6 artisan 命令,它通过一个简单的 GET 请求调用外部 API 来获取一些数据。我想测试这个而不实际调用任何 API。 (我想离线,仍然有测试测试绿色)。

现在简单解释一下逻辑是如何安排的。

1) 这个工匠命令 (php artisan export:data) 有它自己的构造函数 (__construct) 我在其中注入了一堆东西。我注入的其中一件事是ExportApiService 类。很简单。

public function __construct(
    ExportApiService $exportApiService,
) 
    parent::__construct();

    $this->exportApiService = $exportApiService;

2) 现在ExportApiService 扩展了abstract class AbstractApiService。在这个抽象类中,我创建了一个构造函数,我只需将GuzzleHttp\Client 注入$client 属性中,以便在ExportApiService 中我可以使用$this->client 调用API。很简单。

3) 所以我的ExportApiService 有一个名为exportData 的方法。琐碎的。有效。

public function exportData(array $args = []): Collection

   $response = $this->client->request('GET', 'https://blahblah.blah');

   return collect(json_decode($response->getBody()->getContents(), true));

4) 所以最后在我的工匠命令中(我在第 1 点中介绍过)我称之为:

public function handle()

    $exportedData = $this->exportApiService->exportData($this->args);

    $this->info('Exported ' . count($exportedData) . ' records');

我想测试输出是否符合我的期望根本不调用该外部 API。我想以某种方式嘲笑它……

public function testExportSuccessful()

    $this->console->call('export:data', [$some_arguments]);

    $output = $this->console->output();

    $this->assertContains('Exported 123 records', $output); // this does not work :(

现在我的确切问题 - 如何强制 Laravel / PHPUnit 在每次工匠命令调用 exportData() 时返回一个固定值?

你尝试了什么?

我尝试使用以下代码创建setUp() 方法:

public function setUp()

    parent::setUp();

    $mock = $this->createMock(ExportApiService::class);

    $mock->method('exportData')->willReturn(123); // give me a dummy integer each time exportData is being called so that I can use is in the assert at the end

但这根本不起作用,但对我来说很有意义。

感谢您的帮助。

【问题讨论】:

【参考方案1】:

您创建了一个模拟对象,但没有将它绑定到容器中。如果没有绑定,当 Laravel 运行你的命令时,它只会生成一个新的 ExportApiService 实例。

public function setUp()

    parent::setUp();

    $mock = $this->createMock(ExportApiService::class);

    $mock->method('exportData')->willReturn(123);

    // bind the mock in the container, so whenever you ask for
    // a new ExportApiService, you'll get your mocked object.
    $this->app->instance(ExportApiService::class, $mock);


你有另一个选择,而不是这样做,是模拟你的 Guzzle 请求。这样,您的所有代码都可以正常执行,但是当您进行 API 调用时,Guzzle 会返回一个预先确定的响应,而不是实际进行调用。你可以找到Guzzle docs on testing here。

使用模拟响应配置客户端后,您可以将该客户端实例绑定到容器中,以便在创建 ExportApiService 时,Laravel 会使用模拟响应注入您设置的客户端。

【讨论】:

【参考方案2】:

您创建了一个模拟,但这个模拟永远不会到达您的代码。我只是躺在身边。为确保它到达您的代码,您必须了解将类放入构造函数时会发生什么。

当你将一个类放入你的构造中时,Laravel 会向 ioc 容器请求所请求类的实例。如果不可用,它将尝试为您生成它。这就是你可以模拟的地方。

基本上你所要做的就是让 Laravel 知道在需要特定类时应该使用哪个实例。您需要做的就是在创建模拟后添加这一行:

app()->instance(ExportApiService::class, $mock);

这样你告诉 Laravel 每次你需要 ExportApiService 时,Laravel 应该返回 $mock

【讨论】:

以上是关于如何为调用外部 API 的 Laravel Artisan 命令编写 PHPUnit 测试,而无需物理调用该 API?的主要内容,如果未能解决你的问题,请参考以下文章

如何为邮递员 API 请求编写 Laravel PHPUnit 测试

从控制器调用外部 API 函数,LARAVEL 4

从 laravel 调用 HTTPS 请求到外部 API

如何在 ECS Fargate 部署的容器外部进行外部 api 调用

减少对外部 API 的身份验证调用(Laravel 5.6)

Laravel API 测试 - 如何测试具有外部 API 调用的 API