👹

【Laravel テスト】フォームリクエスト 複数項目バリデーションのテストコードを書いてみた

2020/11/03に公開

更新履歴

2020/11/2

はじめに

現在個人開発でLaravelを使って「みんなのポートフォリオまとめサイト」を作っています。

今回の開発にて始めてテストコードを書いているのですが、バリデーションのテストコードは【Laravel】フォームリクエストバリデーションのテストコード作成を参考にさせていただき、実装しました。

(フォームリクエストってなんじゃ!?て方は、Laravelのバリデーションにはフォームリクエストを使おうが大変参考になります!バリデーションも全部コントローラーに書いちゃってたあの頃が懐かしい。。。)

自分で実装していてちょっと詰まったのが、上記記事のサンプルコードでは1項目のバリデーションだったのに対し、今回は複数項目(ユーザー登録のバリデーションだったので、name, email, password の3つ)のバリデーションが必要でした。

リファクタの余地は多分にあると思いますが、今回実装した複数項目バリデーションの書き方を紹介します。
(※95%は【Laravel】フォームリクエストバリデーションのテストコード作成の内容を参考にさせていただきました!ありがとうございます!!)

環境

Laravel 7.26.1
PHP 7.4.11

フォームリクエストの作成

$ php artisan make:request RegisterRequest

ユーザー登録用のバリデーションということでRegisterRequestという名前にします。
(今回はSPA用に認証系のバリデーションも自分で書き換えました。書き換える必要はなかったかもしれない。まあ練習になったからヨシ!)

上記コマンドで、app/Http/Requests/ 配下にファイルRegisterRequest.phpが作成されます。

フォームリクエストの編集

作成されたRegisterRequest.phpを以下のように書き換えます!

app/Http/Requests/RegisterRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class RegisterRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|min:8|confirmed',
        ];
    }

    public function messages()
    {
        return [
            'name.required' => 'ユーザー名を入力してください',
            'name.string' => '正しい形式で入力してください',
            'name.max' => '文字数をオーバーしています。',
            'email.required' => 'メールアドレスを入力してください。',
            'email.email' => '正しい形式でメールアドレスを入力してください',
            'email.max' => '文字数をオーバーしています。',
            'email.unique' => '登録済みのユーザーです',
            'password.required' => 'パスワードを入力してください',
            'password.min' => 'パスワードは8文字以上で入力してください。',
            'password.confirmed' => 'パスワードが一致しません。',
        ];
    }

    /**
     * [override] バリデーション失敗時ハンドリング
     *
     * @param Validator $validator
     * @throw HttpResponseException
     * @see FormRequest::failedValidation()
     */
    protected function failedValidation(Validator $validator)
    {
        $response['status']  = 422;
        $response['statusText'] = 'Failed validation.';
        $response['errors']  = $validator->errors();
        throw new HttpResponseException(
            response()->json($response, 200)
        );
    }
}

rules()メソッドにバリデーションルールを記載します。

'name' => 'required|string|max:255',

たとえばこれは、postされたname = 'name'要素に対して、「入力必須」「文字列型」「最大文字数255」のバリデーションが適用されます。

messages()メソッドに、バリデーションエラー時のメッセージを記載します。

'name.required' => 'ユーザー名を入力してください',
'name.string' => '正しい形式で入力してください',
'name.max' => '文字数をオーバーしています。',

'required|string|max:255'のそれぞれのルールについてエラー時のメッセージを設定していくイメージです。

今回はSPAで非同期通信を行うので、failedValidation()メソッドにてバリデーションエラー時のハンドリングをオーバーライドします。

フロント側で、

javascript

response(任意の変数名).data.errors

でエラーメッセージを取り出すことができます。
スクリーンショット 2020-11-01 23.36.34.png

フォームリクエストのテストファイル作成

$ php artisan make:test RegisterRequestTest

上記コマンドでRegisterRequestTest.phpを作成します。
デフォルトでは、tests/Feature以下に作成されますが、リクエストのテストはユニットテスト扱いということで、tests/Unit配下にRequestsディレクトリを作成します。そして、Requests配下に、RegisterRequestTest.phpを移動します。

tests/Unit/Requests/RegisterRequestTest.php

テストコード編集

以下のようにテストコードを編集します。

tests/Unit/Requests/RegisterRequestTest.php
<?php

namespace Tests\Unit\Requests;

use Illuminate\Support\Facades\Validator;
use App\Http\Requests\RegisterRequest;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;


class RegisterRequestTest extends TestCase
{
    use RefreshDatabase;

    /**
     * カスタムリクエストのバリデーションテスト
     *
     * @param array 項目名の配列
     * @param array 値の配列
     * @param boolean 期待値(true:バリデーションOK、false:バリデーションNG)
     * @dataProvider dataUserRegistration
     */
    public function testUserRegistration(array $keys, array $values, bool $expect)
    {
        //入力項目の配列($keys)と値の配列($values)から、連想配列を生成する
        $dataList = array_combine($keys, $values);

        $request = new RegisterRequest();
        //フォームリクエストで定義したルールを取得
        $rules = $request->rules();
        //Validatorファサードでバリデーターのインスタンスを取得、その際に入力情報とバリデーションルールを引数で渡す
        $validator = Validator::make($dataList, $rules);
        //入力情報がバリデーショルールを満たしている場合はtrue、満たしていな場合はfalseが返る
        $result = $validator->passes();
        //期待値($expect)と結果($result)を比較
        $this->assertEquals($expect, $result);
    }

    public function dataUserRegistration()
    {
        return [
            'OK' => [
                ['name', 'email', 'password', 'password_confirmation'],
                ['testuser', 'test@example.com', 'password', 'password'],
                true
            ],
            '名前必須エラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                [null, 'test@example.com', 'password', 'password'],
                false
            ],
            '名前形式エラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                [1, 'test@example.com', 'password', 'password'],
                false
            ],
            '名前最大文字数エラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                [str_repeat('a', 256), 'test@example.com', 'password', 'password'],
                false
            ],
            'OK' => [
                ['name', 'email', 'password', 'password_confirmation'],
                [str_repeat('a', 255), 'test@example.com', 'password', 'password'],
                true
            ],
            'email必須エラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                ['testuser', null, 'password', 'password'],
                false
            ],
            'email形式エラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                ['testuser', 'test@example.', 'password', 'password'],
                false
            ],
            'email最大文字数エラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                ['testuser', str_repeat('a', 255) . '@example.com', 'password', 'password'],
                false
            ],
            'emailユニークエラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                ['testuser', $this->user->email, 'password', 'password'],
                false
            ],
            'password必須エラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                ['testuser', 'test@example.com', '', ''],
                false
            ],
            'password最小文字数エラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                ['testuser', 'test@example.com', 'pass', 'pass'],
                false
            ],
            'password一致エラー' => [
                ['name', 'email', 'password', 'password_confirmation'],
                ['testuser', 'test@example.com', 'password', 'password1'],
                false
            ],
        ];
    }
}

dataUserRegistration()メソッドにテストケースを書き、それぞれのケースについてtestUserRegistration()メソッドにて実際にテストを実行します。

テストケースの具体例を1つ見てみます。

public function dataUserRegistration()
    {
        return [
            'OK' => [
                ['name', 'email', 'password', 'password_confirmation'], //第一引数
                ['testuser', 'aaa@gmail.com', 'password', 'password'], //第二引数
                true  //第三引数
            ],
     }

バリデーションのkeyを格納した配列を第一引数、バリデーションのvalueを格納した配列を第二引数、バリデーション結果の期待値の真偽値を第三引数にセットし、testUserRegistration()へ渡されます。

テストメソッドのtestUserRegistration()は、上記3つの引数を受け取ります。


    /**
     * カスタムリクエストのバリデーションテスト
     *
     * @param array 項目名の配列
     * @param array 値の配列
     * @param boolean 期待値(true:バリデーションOK、false:バリデーションNG)
     * @dataProvider dataUserRegistration
     */
    public function testUserRegistration(array $keys, array $values, bool $expect)
    {
     //
    }

testUserRegistration()メソッド内のそれぞれの処理内容は、コメントに書いてある通りです。(雑ですみません!)

注意ポイント

uniqueルールにはuse RefreshDatabaseが必要

1つ目の注意ポイントとしては、フォームリクエストRegisterRequest.phpのバリデーションルールにて

app/Http/Requests/RegisterRequest.php
public function rules()
{
     return [
         'name' => 'required|string|max:255',
         'email' => 'required|email|max:255|unique:users',
         'password' => 'required|min:8|confirmed',
     ];
}

テスト時はこちらの記事(【Laravel】PHPUnitテスト用にDBを設定してデフォルトのDBを汚さなくする)を参考にSQLiteのインメモリ機能を使うことで実際のデータベースを汚染しない設定にしており、テストでは基本的にデータベースとの通信は発生しないのですが、今回はemailuniqueルールがあり、データベースとの通信が発生します。

'email' => 'required|email|max:255|unique:users',

use RefreshDatabaseがないと、以下のようなエラーが出ます

SQLSTATE[HY000]: General error: 1 no such table: users (SQL: select count(*) as aggregate from "users" where "email" = aaa@gmail.com)

Illuminate\Foundation\Testing\RefreshDatabaseuseし、クラス内でuse RefreshDatabase;を記載することで、テストメソッド実行前に

$ php artisan migrate:fresh

が実行されます。

tests/Requests/RegisterRequestTest.php
<?php

namespace Tests\Requests;

use Illuminate\Support\Facades\Validator;
use App\Http\Requests\RegisterRequest;
use Illuminate\Foundation\Testing\RefreshDatabase; //←これが必要
use Tests\TestCase;


class RegisterRequestTest extends TestCase
{
    use RefreshDatabase;  //←これが必要

命名規則

2つ目の注意ポイントはテストコードメソッドの命名規則です。

テストメソッドは、接頭辞に「test***」を付けなければいけません(でないとエラーになります)。
また、プロバイダーメソッドdataUserRegistration()は、テストメソッドのdocコメントに@dataProviderアノテーションで指定しなければいけません(指定しないと同じくエラーになります)。

詳細はこちら
データプロバイダ

tests/Requests/RegisterRequestTest.php
    /**
     (中略)
     * @dataProvider dataUserRegistration //←これです
     */
    public function testUserRegistration(array $keys, array $values, bool $expect)
    {

(参考) phpunit.xmlの編集

今回は、RegisterRequestTest.phptests/Unit/Requests配下に配置したので問題ないのですが、たとえばtests/Requests配下にRegisterRequestTest.phpを配置した場合は、tests/Requests配下にあるものがテストコードだということを認識させるために、phpunit.xmlを編集する必要があります。

phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="bootstrap/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Feature Tests">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>

        <testsuite name="Unit Tests">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>

        <testsuite name="Unit Tests">
            <directory suffix="Test.php">./tests/Requests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
    </php>
</phpunit>

phpunit.xmlファイル自体は、Laravelのプロジェクトにデフォルトで用意されており、
上記サンプルコードで追加したのは、以下の部分になります。

<testsuite name="Unit Tests">
    <directory suffix="Test.php">./tests/Requests</directory>
</testsuite>

./tests/Requests配下にあるのはテストコードであることが認識されるようになります。

テストコード実行

$ php artisan test

$ php artisan testはLaravel7系から使えるメソッドらしいので、それ以前のバージョンの方は

$ vendor/bin/phpunit

で代用できます。

その他

テストケースをいろんなパターン用意するのが大変だったんですが、他にいい方法あるんですかね?あったら教えていただきたいです!

今回はname, email, passwordの3つだったからよかったものの、実際にはもっと多いパターンも全然あるわけで、そうなると指数関数的に増えていくよなと。。

おまけ

冒頭ちらっとお話しましたが、現在「みんなのポートフォリオまとめサイト」を制作中です。
自分がプログラミング初学者だったころ、「他の人はどんなポートフォリオを作っているのか」がとても知りたかったんですが、ほとんどその情報にたどり着くことはできませんでした(未だに無い、、と思ってる)。
「じゃあ自分で作っちゃおう!」と、自分の技術のアウトプットも併せてReact×Laravelを使ってSPAで作っております。

完成まであと1,2ヶ月はかかりそうですが、なんとかお届けできるよう頑張ります!

「みんなのポートフォリオまとめサイト」の制作日記
【第0回】「みんなのポートフォリオまとめサイト」を作ります~宣言編~
【第1回】「みんなのポートフォリオまとめサイト」を作ります~着手編~
【第2回】「みんなのポートフォリオまとめサイト」を作ります~SPA認証で死闘編~

Discussion