60
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-02

更新履歴

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認証で死闘編~

60
55
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?