Httpファサードを使って、外部APIのモックテストを作りたい!
インターン生の望月です。
つい最近外部APIを使用する部分のテストを作成しました。
テストを書き慣れていないことと、外部APIとの通信方法の部分がいまいち理解できていなかったこともあり苦戦しましたが、LaravelでのHttpファサードを用いたテスト方法を少し理解できたのでまとめてみようと思います。
0. 概要
- Http::fake()でモックできるのはHttpファサードを使用したリクエストのみ
(GuzzleHTTPクライアントでのリクエストやリダイレクトはモックできないよ!) - fakeメソッドで静的なAPIレスポンスを設定する場合には引数にarrayを、動的なAPIレスポンスを設定する場合には引数にclosureを使おう
1. Httpファサードとは
今回使用するのは、Illuminate\Support\Facades\Httpになります。
こちらのファサードはLaravel 7.Xで登場しました。
このHttpファサードは、内部でGuzzleHTTPクライアントをラップしているため、Guzzleの機能を容易に使用できるようにしてくれています。
つまり、Httpファサードを使うことで、簡単にHTTPリクエストの送受信を行うことができる!
ということになります。
例えば、単純なGETリクエストを送る場合で比較してみます。
<?php
namespace Tests\Feature;
use Tests\TestCase;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use Illuminate\Support\Facades\Http;
class HttpMockTest extends TestCase
{
public function testSendGetRequest()
{
//GuzzleHttp
$client = new Client();
try {
$response = $client->request('GET', 'https://example.com');
} catch (ClientException $e) {
//4xxエラーが出た場合
} catch (ServerException $e) {
//5xxエラーが出た場合
}
//Http Facade
$response = Http::get('https://example.com');
if ($response->clientError()) {
//4xxエラーが出た場合
} elseif ($response->serverError()) {
//5xxエラーが出た場合
}
}
}
このように元から便利なGuzzleHttpでしたが、Httpファサードによってさらに簡単に使うことができます。
cURLでのリクエスト送信例
<?php
namespace Tests\Feature;
use Tests\TestCase;
class CurlMockTest extends TestCase
{
public function testSendGetRequest()
{
$curl = curl_init();
$url = "https://example.com";
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, false);
$response = curl_exec($curl);
if (curl_errno($curl)) {
echo 'cURL Error: ' . curl_error($curl);
}
curl_close($curl);
}
}
2. APIモックテストで使用するメソッド:fake(array)
Httpファサードでは、外部へのリクエストをモックしてテストできるようなメソッドが提供されています。
それがfakeメソッドになります。
まずは、簡単な使用例を書いてみます。
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class ApiMockTest extends TestCase
{
public function testSendGetRequest()
{
Http::fake([
'example.com' => Http::response('Hello World!', 200),
'example.com/api' => Http::response(['name' => 'Lucy'], 200),
'*' => Http::response('not found', 404),
]);
}
}
このような形で定義しておくことで、特定のURLに対するリクエストをインターセプトし、あらかじめ決めておいたレスポンスを返すようにできます。
今回の例の場合だと、https://example.com
に対するリクエストを送ると、Hello World!という文字列が返って来て、https://example.com/api
に対するリクエストを送ると、['name' => 'Lucy']という連想配列が返って来ます。
また、それ以外へのリクエストは全て、ステータスコード404で、not foundという文字列が返って来ます。
このメソッドを使うことで、外部へのリクエストをせずに、予想されるレスポンスを得ることができるようになります。
3. 動的にレスポンスを返すモックテスト:fake(Closure)
先ほどは単純なHTTPレスポンスを設定していたので、簡単に設定できましたが、テストごとに設定するパラメータを変えて、パラメータごとに違うレスポンスを返して欲しいということもあると思います。
そうなったら、レスポンスを設定していた部分にパラメータを処理して、適切な内容のレスポンスを返すようなカスタムメソッドを用意しましょう。
今回は、https://example.com
に対するレスポンスをカスタムメソッドで処理してみます。
<?php
namespace Tests\Feature;
use GuzzleHttp\Promise\PromiseInterface;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
class ApiMockTest extends TestCase
{
protected int $parameter;
public function testSendGetRequest()
{
Http::fake([
'example.com' => $this->customResponse($request),
'example.com/api' => Http::response(['name' => 'Lucy'], 200),
'*' => Http::response('not found', 404),
]);
$this->parameter = 200;
$response = Http::get('https://example.com', ['parameter' => $this->parameter]);
}
private function customResponse($request): PromiseInterface
{
//GETリクエストの場合
//POSTやPUTの場合はbody()を使って取得する
$query_string = parse_url($request->url(), PHP_URL_QUERY);
parse_str($query_string, $param);
$status = 200;
switch($param['parameter']){
case 100:
Log::info('100');
break;
case 200:
Log::info('200');
break;
case 300:
Log::info('300');
break;
default:
$status = 404;
Log::info('not found');
}
return Http::response(['param' => $param['parameter']], $status);
}
}
このような形にすることで、一見$this->parameter
の値を変えることで、返ってくるレスポンスを変えることができるように見えますが、このコードではエラーが起きてしまいます。
そもそも、fakeメソッドがどのようなものかを考えてみましょう。
コードに注目すると、URLをkey、レスポンスをvalueとした連想配列を設定しています。
fakeメソッドに配列を渡した場合は、fakeが読みこまれた時点でURLとレスポンスを読み込むので、後からレスポンスの内容を変更できません。
つまり、この方法では静的なモックの場合には問題ないのですが、入力内容に基づいたモックを作成する上では難しいということになります。
ではどうするのか、ということですが、
fakeメソッドの引数には配列(array)か無名関数(Closure)を渡すことができます。
fakeメソッドの定義(公式ドキュメントより)
Closureを渡すと、HTTPリクエストを行った段階でClosureが呼び出されるので、レスポンスの内容を入力したパラメータからレスポンスを生成できるようになります。
では、Closureを使って書き直してみます。
<?php
namespace Tests\Feature;
use GuzzleHttp\Promise\PromiseInterface;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
class ApiMockTest extends TestCase
{
protected int $parameter;
public function testSendGetRequest()
{
Http::fake(function ($request) {
switch ($request->url()) {
//今回はGETリクエストなので、クエリパラメータの部分もURLに指定する必要がある
case 'https://example.com?parameter=' . $this->parameter:
return $this->customResponse($request);
case 'https://example.com/api':
return Http::response(['name' => 'Lucy'], 200);
default:
return Http::response('not found', 404);
}
});
$this->parameter = 200;
$response = Http::get('https://example.com', ['parameter' => $this->parameter]);
}
private function customResponse($request): PromiseInterface
{
//GETリクエストの場合
//POSTやPUTの場合はbody()を使って取得する
$query_string = parse_url($request->url(), PHP_URL_QUERY);
parse_str($query_string, $param);
$status = 200;
switch($param['parameter']){
case 100:
Log::info('100');
break;
case 200:
Log::info('200');
break;
case 300:
Log::info('300');
break;
default:
$status = 404;
Log::info('not found');
}
return Http::response(['param' => $param['parameter']], $status);
}
}
このようにすることで、リクエストのパラメータに対応したレスポンスを生成できます。
4. まとめ
今回はHttpファサードのfakeメソッドの使い方を軽くまとめました。
fakeメソッドの引数を上手く使い分けることで、複雑なテストでも作成できるようになります。
テストの実行時にはリダイレクトは自動でインターセプトしてくれますが、外部APIへのリクエストはインターセプトしてくれないので、fakeメソッドを使って、外部サービスなしでも実行できるモックテストを作りましょう!
Discussion