如何为调用外部 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 测试
如何在 ECS Fargate 部署的容器外部进行外部 api 调用