phpunit で ajax リクエストかどうか判定したい.
結構ハマったのでメモ.
概要は次の通り.
動作環境
- php 7.2.x
- Laravel 5.5.x
- phpunit 6.5.x
前提条件 & 事前知識
前提条件
- phpunit の基本的な使い方は先日まとめたこちらの記事を参照.
- Laravel のバージョンによって project dir. 構造が多少異なるので注意.
事前知識
いくつか知っておいた方がいい知識を紹介.
- Laravel の Request class は
Symfony\Component\HttpFoundation\Request
を拡張している - ajax 判定は Laravel ヘルパの
$request->ajax()
で可能 - unit テスト内では middleware をインスタンス化し,
handle()
の引数に任意の closure を渡すことが可能
Laravel の Request class は Symfony\Component\HttpFoundation\Request
を拡張している
Laravel の公式ドキュメントには次のように書かれている.
lluminate\Http\Requestインスタンスは、Symfony\Component\HttpFoundation\Requestクラスを拡張しており
引用: https://readouble.com/laravel/5.5/ja/requests.html
今回作成するテストでは, 親クラスのメソッドを利用する.
必要に応じて Symfony\Component\HttpFoundation\Request
のソースを眺めておくといいかも.
- https://github.com/symfony/symfony/blob/2.7/src/Symfony/Component/HttpFoundation/Request.php
ajax 判定は Laravel ヘルパの $request->ajax()
で可能
これは Laravel API ドキュメントに載っている.
bool
ajax()
Determine if the request is the result of an AJAX call.引用: https://laravel.com/api/4.2/Illuminate/Http/Request.html
ajax リクエストの動作確認には postman 等の HTTP Client Tool がよく用いられる.
- https://www.getpostman.com/
こういうツールは便利なんだけど, 設定周りでハマる事が多い. ログイン認証したいときとか.
今回はコマンドラインから curl を実行して動作確認を行う.
ヘッダを編集することで ajax からのリクエストのような振る舞いが可能.
たとえばこんな感じで↓
$ curl -H "X-Requested-With: XMLHttpRequest" リクエスト先
middleware 内で $request->ajax()
が期待通りの動作をするかちょろっと確認したいならこれで十分.
unit テスト内では middleware をインスタンス化し, handle()
の引数に任意の closure を渡すことが可能
ぱっと middleware の method を眺めると handle()
がある.
phpunit 内で new
でインスタンス化しらた, 任意の closure を handle()
の引数に渡そう.
今回は対象の middleware を通過した合図として, true
を返す closure を設定した.
この点については後述のコードを参照.
今回やりたいこと
phpunit でサイトへのアクセスが ajax によるものか, それ以外かを判定するテストがしたい.
事前準備
ひとまず route へのアクセス時に通過する before middleware を作っておく.
<php
// app/Http/Middleware/SampleBeforeMiddleware.php
namespace App\Http\Middleware;
use Closure;
class SampleBeforeMiddleware
{
public function handle($request, Closure $next)
{
if ($request->ajax()) {
return redirect('/', 301); // ajax リクエストだったら 301 リダイレクトする
}
return $next($request);
}
}
↑コレ は ↓こんな middleware
- ajax リクエストを受けたら 301 redirect する middlewar
middleware を作ったら Kernel.php に登録するのを忘れずに.
試しに curl で ajax リクエストをしてみる.
もし期待通りの動作なら, response header に 301 redirect の表示があるはず.
curl で response header 1行目 を取得してみる.
$ curl -D - -o /dev/null -skH "X-Requested-With: XMLHttpRequest" リクエスト先 | head -1
HTTP/1.1 301 Moved Permanently
curl オプションを次のようにすることで擬似的に ajax リクエストの生成が可能.
期待通りにリダイレクトされていることが判る.
-H "X-Requested-With: XMLHttpRequest"
ここからは phpunit の準備をする.
phpunit で middleware のテストをする
middleware のテストでポイントになるのはこのへん.
- ajax の request instance を生成する
- closure は単に
true
を返すようにする (これはわかりやすければ何でもいい)
ちょっと解説.
ajax の request instance を生成する
Laravel の Request クラスは ↓コレ を継承している.
Symfony\Component\HttpFoundation\Request
↑コレ を利用すると, 期待に沿った request instance を作りやすい.
たとえば, 今回は ajax リクエストを擬似的に作りたいのでこんな具合にする↓
$request = Request::create('/', 'GET');
$request->headers->set('X-Requested-With', 'XMLHttpRequest');
先程の curl の例と同様に, request header に ajax であることを示す値をセットした.
これで擬似的に ajax リクエストのインスタンス $request
が完成.
closure は単に true
を返すようにする
これ, なんでもいいんだけど私は理解しやすいので true
を返す closure を作成した.
Laravel の middleware に記述されている return $next($request);
は 「次の処理に行くよー」 って意味.
ここでいう次の処理とは
- 次の middleware に処理が移る
- controller に処理が移る
のいずれかの意味.
「この middleware の処理は終わったよー」 という合図として認識しやすいものであればなんでも ok.
こんな考えで, 私は次のようなテストを書いた.
<?php
// tests/SampleBeforeMiddlewareTest.php
use App\Http\Middleware\SampleBeforeMiddleware;
use Illuminate\Http\Request;
class SampleBeforeMiddlewareTest extends TestCase
{
private $closure;
protected function setUp()
{
parent::setUp();
$this->closure = function () {
return true;
};
}
public function testRedirect()
{
$request = Request::create('/', 'GET');
$request->headers->set('X-Requested-With', 'XMLHttpRequest');
$sampleBeforeMiddleware = new SampleBeforeMiddleware();
$response = $sampleBeforeMiddleware->handle($request, $this->closure);
$this->assertInstanceOf(SampleBeforeMiddleware::class, $sampleBeforeMiddleware);
$this->assertSame(301, $response->status());
}
}
多くの場合, phpunit で実行するテストは1つじゃない.
だから, テスト間で使い回せるように setUp()
に closure を定義した.
ここで定義した closure は middleware インスタンスから呼び出された handle()
method 第2引数に渡されている.
assertion は次の2点.
- 期待した middleware の instance が生成されているか
- response の HTTP Status Code は 301 か
ここまでで ajax に関するテストは完成.
実行
phpunit を実行しようと思うんだけど, 折角なので次のテストも行う.
ajax リクエスト ではなく, 単なる GET リクエストが来たらこの middleware を素通りするテスト
これは closure に設定した返り値 true
を見れば良い.
最終的なテストファイルはこれ↓
<?php
// tests/SampleBeforeMiddlewareTest.php
use App\Http\Middleware\SampleBeforeMiddleware;
use Illuminate\Http\Request;
class SampleBeforeMiddlewareTest extends TestCase
{
private $closure;
protected function setUp()
{
parent::setUp();
$this->closure = function () {
return true;
};
}
public function testRedirect()
{
$request = Request::create('/', 'GET');
$request->headers->set('X-Requested-With', 'XMLHttpRequest');
$sampleBeforeMiddleware = new SampleBeforeMiddleware();
$response = $sampleBeforeMiddleware->handle($request, $this->closure);
$this->assertInstanceOf(SampleBeforeMiddleware::class, $sampleBeforeMiddleware);
$this->assertSame(301, $response->status());
}
public function testSkipMiddleware()
{
$request = Request::create('/', 'GET');
// $request->headers->set('X-Requested-With', 'XMLHttpRequest');
$sampleBeforeMiddleware = new SampleBeforeMiddleware();
$response = $sampleBeforeMiddleware->handle($request, $this->closure);
$this->assertInstanceOf(SampleBeforeMiddleware::class, $sampleBeforeMiddleware);
$this->assertSame(true, $response);
}
}
testSkipMiddleware()
はこの middleware を skip するテスト.
ajax リクエストのテストと比べて closure の値を見ているのが特徴.
いざ, 実行.
# cd path/to/your/phpunit/test/directory
$ phpunit ./SampleBeforeMiddlewareTest.php
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.
. . 2 / 2 (100%)
Time: 176 ms, Memory: 10.00MB
OK (2 tests, 4 assertions)
ok.
まとめ
phpunit で ajax request instance を作るのが最初のハードル.
次のハードルは, closure をよしなに取り扱うこと.
多くの時間を調査に割いたけど, 前者に辿り着くのが大変だった.
結果的に公式ドキュメントに Request インスタンスの親クラスについての記述があったから, ドキュメントをよく読むのは大切だと再認識した.
Laravel の API ドキュメント眺めると発見が多いな.
今回は以上.