更新履歴
2020/11/2
-
RegisterRequestTest.php
ファイルの配置場所をtests/Requests/
配下からtests/Unit/Requests/
へ変更 -
phpunit.xml
の編集に関する記述を修正 - テストケースのサンプルメールアドレスを
test@example.com
に修正(test.comやaaa.comをテストデータに使うのはやめましょうという話)
はじめに
現在個人開発で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
を以下のように書き換えます!
<?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()
メソッドにてバリデーションエラー時のハンドリングをオーバーライドします。
フロント側で、
response(任意の変数名).data.errors
フォームリクエストのテストファイル作成
$ php artisan make:test RegisterRequestTest
上記コマンドでRegisterRequestTest.php
を作成します。
デフォルトでは、tests/Feature
以下に作成されますが、リクエストのテストはユニットテスト扱いということで、tests/Unit
配下にRequests
ディレクトリを作成します。そして、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
のバリデーションルールにて
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のインメモリ機能を使うことで実際のデータベースを汚染しない設定にしており、テストでは基本的にデータベースとの通信は発生しないのですが、今回はemail
のunique
ルールがあり、データベースとの通信が発生します。
'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
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
アノテーションで指定しなければいけません(指定しないと同じくエラーになります)。
詳細はこちら
データプロバイダ
/**
(中略)
* @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
を編集する必要があります。
<?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認証で死闘編~