テストするときにミドルウェアを無効化する記事はあっても、ミドルウェアそのものをテストする記事があまり無かったので、書いてみます。
前提
- Laravel 6.18.10
- PHP 7.3.15
ミドルウェア
ここでは、リクエストに含まれるバージョンをチェックするミドルウェアを作ってみます。1 API という前提で書きますが、基本的な考え方は Web でも同じだと思います。
リクエストに含まれている version
が MIN_VERSION
2 以上である場合は、許可します。
一方、そもそも version
が送信されなかったり、 version
が MIN_VERSION
よりも小さい場合は、エラーの JSON レスポンスを返します。3
namespace App\Http\Middleware;
use Closure;
class VersionCheck
{
/**
* 利用可能な最低バージョン
*/
private const MIN_VERSION = '1.0.1';
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$version = $request->get('version');
if ($version === null || $version < self::MIN_VERSION) {
return response()->json([
'message' => 'このバージョンは利用できません。'
], 400);
}
return $next($request);
}
テスト
ミドルウェアが出来たので、テストコードを作っていきます。
正常系
まずは、リクエストを作ります。
$request = app()->make('request');
$request->merge(['version' => '1.0.1']);
そして、ミドルウェアを実行します。
$middleware = new VersionCheck();
$middleware->handle($request, function () {
$this->assertTrue(true);
});
本来、 handle()
の第2引数は、バージョンチェックの次に動くミドルウェアを指定します。
このことは、ミドルウェアの雛形を見ると、よく分かるかと思います。
public function handle($request, Closure $next)
{
// ...
// 処理が全部終わったら $next で渡された関数を実行する。
return $next($request);
}
そのため、 handle()
の第2引数の中で assertTrue(true)
と書くことで、 バージョンチェックが全て終了して次のミドルウェアへ進むこと をテストすることができます。
しかし、これではまだ不十分です。 エラーレスポンスが返ってこないこと をテストできていないからです。
バージョンチェックが正常な場合、本来ならば、ミドルウェアの後に動くコントローラがレスポンスを返します。
しかし、ここではミドルウェアしか動かしていないため、レスポンスは返ってきません。 つまり、 handle()
の戻り値が null
であることをチェックすれば十分ということになります。
ということで、正常系のテストコードは以下のようになります。
/**
* @test
*/
public function 利用可能なバージョン(最低バージョンと等しい)である場合は正常終了すること()
{
$request = app()->make('request');
$request->merge(['version' => '1.0.1']);
$middleware = new VersionCheck();
$response = $middleware->handle($request, function () {
$this->assertTrue(true);
});
// エラーレスポンスが返却されないこと
$this->assertNull($response);
}
異常系
正常系と違って、異常系ではエラーレスポンスのテストが必要になります。
ここで問題となるのは、 $response->assertStatus()
のようなメソッドが 使えない ということです。
通常、コントローラのテストコードで get()
や post()
を記載すると、自動的に TestResponse
クラスのインスタンスが返却されます。 assertStatus()
は TestResponse
クラスのメソッドなので、特に意識することなく使うことができます。
ところが、ミドルウェアの handle()
は TestResponse
ではなく Response
クラスのインスタンスを返却します。そのため、 assertStatus()
を使うことはできません。
もちろん、以下のように書くことはできます。
$this->assertSame(400, $response->getStatusCode());
$this->assertSame([
'message' => 'このバージョンは利用できません。'
], json_decode($response->getContent(), true));
しかし、これは、かなり冗長に感じられます。
そこで、get()
や post()
がどのように動いていくかを見ていきました。
その結果、トレイト Illuminate\Foundation\Testing\Concerns\MakesHttpRequests
の createTestResponse()
を呼び出して、 Response
を TestResponse
に格納し直していることが分かりました。
そして、テストクラスの親クラス Illuminate\Foundation\Testing\TestCase
は MakesHttpRequests
を use
しているので、テストクラスからも createTestResponse()
を呼び出せることが分かりました。
こうして、テストコードはこのように書き直すことができました。
$middleware = new VersionCheck();
$originalResponse = $middleware->handle($request, function () {
$this->assertTrue(true);
});
$response = $this->createTestResponse($originalResponse);
$this->assertNotNull($response);
$response->assertStatus(400);
$response->assertExactJson([
'message' => 'このバージョンは利用できません。'
]);
完成形
namespace Tests\Feature\Middleware;
use Tests\TestCase;
use App\Http\Middleware\VersionCheck;
class VersionCheckTest extends TestCase
{
/**
* @test
*/
public function 利用可能なバージョン(最低バージョンと等しい)である場合は正常終了すること()
{
$request = app()->make('request');
$request->merge(['version' => '1.0.1']);
$middleware = new VersionCheck();
$response = $middleware->handle($request, function () {
$this->assertTrue(true);
});
// エラーレスポンスが返却されないこと
$this->assertNull($response);
}
/**
* @test
*/
public function 利用可能なバージョン(最低バージョンより大きい)である場合は正常終了すること()
{
$request = app()->make('request');
$request->merge(['version' => '1.0.2']);
$middleware = new VersionCheck();
$response = $middleware->handle($request, function () {
$this->assertTrue(true);
});
// エラーレスポンスが返却されないこと
$this->assertNull($response);
}
/**
* @test
*/
public function バージョンが送信されない場合はエラーレスポンスが返却されること()
{
$request = app()->make('request');
$middleware = new VersionCheck();
$originalResponse = $middleware->handle($request, function () {
$this->assertTrue(true);
});
$response = $this->createTestResponse($originalResponse);
// エラーレスポンスが返却されること
$this->assertNotNull($response);
// ステータスコードが400であること
$response->assertStatus(400);
// JSONレスポンスが期待どおりであること
$response->assertExactJson([
'message' => 'このバージョンは利用できません。'
]);
}
/**
* @test
*/
public function 利用不可能なバージョンである場合はエラーレスポンスが返却されること()
{
$request = app()->make('request');
$request->merge(['version' => '1.0.0']);
$middleware = new VersionCheck();
$originalResponse = $middleware->handle($request, function () {
$this->assertTrue(true);
});
$response = $this->createTestResponse($originalResponse);
// エラーレスポンスが返却されること
$this->assertNotNull($response);
// ステータスコードが400であること
$response->assertStatus(400);
// JSONレスポンスが期待どおりであること
$response->assertExactJson([
'message' => 'このバージョンは利用できません。'
]);
}
感想
ミドルウェアは便利な反面、どのように書くべきかという「ベストプラクティス」を、あまり見つけることができませんでした。
「もっと良い方法があるよ!」という方は、こっそり教えていただければと思います。
参考
- Laravel 公式ドキュメント
- Testing Middleware in Laravel with PHPUnit