10
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

Laravelのミドルウェアをテストする方法を考えてみた

テストするときにミドルウェアを無効化する記事はあっても、ミドルウェアそのものをテストする記事があまり無かったので、書いてみます。

前提

  • Laravel 6.18.10
  • PHP 7.3.15

ミドルウェア

ここでは、リクエストに含まれるバージョンをチェックするミドルウェアを作ってみます。1 API という前提で書きますが、基本的な考え方は Web でも同じだと思います。

リクエストに含まれている versionMIN_VERSION 2 以上である場合は、許可します。

一方、そもそも version が送信されなかったり、 versionMIN_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\MakesHttpRequestscreateTestResponse() を呼び出して、 ResponseTestResponse に格納し直していることが分かりました。

そして、テストクラスの親クラス Illuminate\Foundation\Testing\TestCaseMakesHttpRequestsuse しているので、テストクラスからも 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' => 'このバージョンは利用できません。'
        ]);
    }

感想

ミドルウェアは便利な反面、どのように書くべきかという「ベストプラクティス」を、あまり見つけることができませんでした。

「もっと良い方法があるよ!」という方は、こっそり教えていただければと思います。

参考


  1. ミドルウェアには、コントローラの に動くタイプと、 に動くタイプの2種類がありますが、ここでは に動くタイプを作ります。 

  2. 本来、 MIN_VERSION は DB 等から取得するべきですが、ここでは簡略化します。 

  3. ここではミドルウェアで JSON レスポンスを返していますが、これが良いのかどうか、あまり自信がありません。特に、Web の場合はエラー画面を表示することになりますが、ミドルウェアで直接ビューを表示せずに、一回リダイレクトしてからコントローラで表示すべきという Stack Overflow の 回答 もありました。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
10
Help us understand the problem. What are the problem?