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

## 更新履歴
### 2020/11/2 

- `RegisterRequestTest.php`ファイルの配置場所を`tests/Requests/`配下から`tests/Unit/Requests/`へ変更
- `phpunit.xml`の編集に関する記述を修正
- テストケースのサンプルメールアドレスを`test@example.com`に修正（[test.comやaaa.comをテストデータに使うのはやめましょうという話](https://blog.ko31.com/201304/sample-domain-example/)）


## はじめに

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


今回の開発にて始めてテストコードを書いているのですが、バリデーションのテストコードは[【Laravel】フォームリクエストバリデーションのテストコード作成](https://qiita.com/n_mogi/items/57a946205df2a69889c2)を参考にさせていただき、実装しました。

（フォームリクエストってなんじゃ！？て方は、[Laravelのバリデーションにはフォームリクエストを使おう](https://qiita.com/sakuraya/items/abca057a424fa9b5a187)が大変参考になります！バリデーションも全部コントローラーに書いちゃってたあの頃が懐かしい。。。）

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

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

## 環境
Laravel 7.26.1
PHP 7.4.11

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

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

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

## フォームリクエストの編集
作成された` RegisterRequest.php `を以下のように書き換えます！

```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()`メソッドにバリデーションルールを記載します。

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


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

```php
'name.required' => 'ユーザー名を入力してください',
'name.string' => '正しい形式で入力してください',
'name.max' => '文字数をオーバーしています。',
```
`'required|string|max:255'`のそれぞれのルールについてエラー時のメッセージを設定していくイメージです。

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

フロント側で、

```javascript:javascript

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

でエラーメッセージを取り出すことができます。
![スクリーンショット 2020-11-01 23.36.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407378/02b47992-e5d2-5208-81d1-5744d383b36b.png)

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

```
$ php artisan make:test RegisterRequestTest
```

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

`tests/Unit/Requests/RegisterRequestTest.php`

## テストコード編集

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

```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()`メソッドにて実際にテストを実行します。

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

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

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

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

```php

    /**
     * カスタムリクエストのバリデーションテスト
     *
     * @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`のバリデーションルールにて

```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を汚さなくする](https://qiita.com/sola-msr/items/b317bb788f21dac176c4)）を参考にSQLiteのインメモリ機能を使うことで実際のデータベースを汚染しない設定にしており、テストでは基本的にデータベースとの通信は発生しないのですが、今回は`email`の`unique`ルールがあり、データベースとの通信が発生します。

```php
'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\RefreshDatabase`を`use`し、クラス内で`use RefreshDatabase;`を記載することで、テストメソッド実行前に 

```
$ php artisan migrate:fresh
```
が実行されます。

```php: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`アノテーションで指定しなければいけません（指定しないと同じくエラーになります）。

詳細はこちら
[データプロバイダ](https://phpunit.de/manual/6.5/ja/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers)

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

## (参考) phpunit.xmlの編集
今回は、`RegisterRequestTest.php`を`tests/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のプロジェクトにデフォルトで用意されており、
上記サンプルコードで追加したのは、以下の部分になります。

```xml
<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`の３つだったからよかったものの、実際にはもっと多いパターンも全然あるわけで、そうなると指数関数的に増えていくよなと。。

## おまけ

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

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

**「みんなのポートフォリオまとめサイト」の制作日記**
[【第0回】「みんなのポートフォリオまとめサイト」を作ります~宣言編~](https://qiita.com/kiwatchi1991/items/58b53c5b4ddf8d6a7053)
[【第1回】「みんなのポートフォリオまとめサイト」を作ります~着手編~](https://qiita.com/kiwatchi1991/items/62ecde8289887965aff3)
[【第2回】「みんなのポートフォリオまとめサイト」を作ります~SPA認証で死闘編~](https://qiita.com/kiwatchi1991/items/2fdd533236cd32237bb2)