前回のFormRequestの入力補正を例にして、そのユニットテストを作成してみます。

ルールの部分だけのテスト

まず、テスト作成のコマンドを実行します。

$ php artisan make:test App/Http/Requests/EventRequestTest 

実行すると、テストのファイルはLaravelのディレクトリ構造を真似て、tests/Feature/App/Http/Requestsのディレクトリに作成されます。

作成されたテストを以下のように編集します。


namespace Tests\Feature\App\Http\Requests;

use App\Http\Requests\EventRequest;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class EventRequestTest extends TestCase
{
    /**
     * @test
     * @dataProvider validationProvider
     */
    public function validation($expected, $data)
    {
        $rules = (new EventRequest())->rules(); // rules()だけを抜き出す
        $validator = validator($data, $rules);

        $this->assertEquals($expected, $validator->passes());
    }

    public function validationProvider()
    {
        return [
            [
                true,
                [
                    'start_at' => '2020-01-01 00:00:00',
                    'end_at'   => '2020-01-01 01:00:00',
                ],
            ],
            [
                false,
                [
                    'start_at' => '2020-01-01 00:00:00',
                    'end_at'   => '2020-01-01 24:00:00', // 24時はない
                ],
            ]
        ];
    }
}

実行すれば、こんな感じでパスです。あとはvalidationProvider()のテストケースを増やしていくだけです。

PHPUnit 8.5.8 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 152 ms, Memory: 22.00 MB

OK (2 tests, 2 assertions)

しかし、ちょっと待ってください!

EventRequestでは以下のようにprepareForValidation()が使用されています。
つまり、実際の入力の項目(date, start_hr, end_hr)を合わせて新規の項目(start_at, end_at)を作成しています。

...
    protected function prepareForValidation()
    {
        // ここでDBに収納する項目を作成
        $this->merge([
            'start_at' => sprintf('%s %02d:00:00', $this->date, $this->start_hr),
            'end_at'   => sprintf('%s %02d:59:59', $this->date, $this->end_hr),
        ]);
    }
...

先のテストでは、ルールだけのテストなのでこの部分のテストをスキップしてしまっているのです。

ベターなテスト

やはり、直接画面から入力されるテストをしたいということで、$this->post('/event')とかの使用も考えましたが、DBデータを用意する必要や認証も考えてやらないやら、ちょいと面倒です。

ということで、以下の記事を見つけました。

https://dev.to/dsazup/testing-laravel-form-requests-853

これを参照して先のテストを変更しました。今度は、datestart_hrend_hrがテストデータとなっていることに注意してください。


namespace Tests\Feature\App\Http\Requests;

use App\Http\Requests\EventRequest;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Validation\ValidationException;

use Tests\TestCase;

class EventRequestTest extends TestCase
{
    /**
     * @test
     * @dataProvider validationProvider
     */
    public function validation($expected, $data)
    {
       $this->assertEquals($expected, $this->validate($data));
    }

    // ここが先の記事からとってきたところ、LaravelのFormRequestServiceProvider::boot()を参照したらしい。
    protected function validate($data)
    {
        $this->app->resolving(EventRequest::class, function ($resolved) use ($data){
            $resolved->merge($data);
        });

        try {
            app(EventRequest::class);

            return true;
        } catch (ValidationException $e) {
            return false;
        }
    }

    public function validationProvider()
    {
        return [
            [
                true,
                [
                    'date'     => '2020-01-01',
                    'start_hr' => '0',
                    'end_hr'   => '1',
                ],
            ],
            [
                false,
                [
                    'date'     => '2020-01-01',
                    'start_hr' => '0',
                    'end_hr'   => '24',
                ],
            ]
        ];
    }
}

実行してみると、

PHPUnit 8.5.8 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 145 ms, Memory: 22.00 MB

OK (2 tests, 2 assertions)

うまいこと成功です!

テストにより問題発見!

テストケースを増やしてみます。

...
    public function validationProvider()
    {
        return [
...
            [
                true,
                [
                    // 同じ時間は、00:00:00と00:59:59となるのでOK
                    'date'     => '2020-01-01',
                    'start_hr' => '0',
                    'end_hr'   => '0',
                ],
           ],
           [
                false,
                [
                    'date'     => '2020-01-01',
                    'start_hr' => 'a',
                    'end_hr'   => 'b',
                ],
            ]
        ];
    }
}
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.

...F                                                                4 / 4 (100%)

Time: 268 ms, Memory: 22.00 MB

There was 1 failure:

1) Tests\Feature\App\Http\Requests\EventRequestTest::validation with data set #3 (false, array('2020-01-01', 'a', 'b'))
Failed asserting that true matches expected false.

あれれ、エラーになりました。 これでは、’2020-01-01′, ‘a’, ‘b’の入力でも正しいということになってしまいます。
これはおかしい。

問題は、EventRequest::prepareForValidation()の部分で、以下のようにsprintf()で文字列が0に変換されることです。tinkerを使って検証してみると、

>>> $data = (object)['date' => '2020-01-01', 'start_hr' => 'a', 'end_hr' => 'b'];
=> {#4452
     +"date": "2020-01-01",
     +"start_hr": "a",
     +"end_hr": "b",
   }
>>> sprintf('%s %02d:00:00', $data->date, $data->start_hr, $data->end_hr);
=> "2020-01-01 00:00:00"

ゼロになっていましたね。

さて、この修正は以下となりました。start_atを作成するFormRequestの関数で、start_hr, end_hrを先にチェックして違法なら、invalidという文字列を与えることにしました。

...
   protected function prepareForValidation()
    {
        if (! preg_match('/^[0-9]{1,2}$/', $this->start_hr)) { // 2桁の数字でないときは、'invalid'とする
            $start_at = 'invalid';
        } else {
            $start_at = sprintf('%s %02d:00:00', $this->date, $this->start_hr);
        }

        if (! preg_match('/^[0-9]{1,2}$/', $this->end_hr)) {
            $end_at = 'invalid';
        } else {
            $end_at = sprintf('%s %02d:59:59', $this->date, $this->end_hr);
        }

        $this->merge([
            'start_at' => $start_at,
            'end_at'   => $end_at,
        ]);
    }
...

実行すると、今度はパスです。

PHPUnit 8.5.8 by Sebastian Bergmann and contributors.

....                                                                4 / 4 (100%)

Time: 203 ms, Memory: 22.00 MB

OK (4 tests, 4 assertions)

By khino