1.はじめに(記事の概要)
LaravelのFormRequestを利用することのメリットの1つに「テストがしやすい」という点があります。
しかし実際にFormRequestのテストを書こうとすると書き方に迷うことも多いです。
この記事ではざっくりとFormRequestの概要を整理したうえで、実際にどのようなテストコードを書けばよいのかをご紹介します。
2.FormRequestの概要
FormRequestはLaravelのバリデーション・認可ロジックをカプセル化するカスタムリクエストクラスのこと
要はユーザーが定義した下記のようなバリデーションロジックを専用クラスに分離し、コントローラでは「バリデーション済みのデータを受け取るだけ」にすることができる
/**
* リクエストに適用するバリデーションルールを取得
*
* @return array
*/
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
3.なぜFormRequestを使うのか?
概要を見ただけでは何のために使うのかイメージがしにくいため、メリットとしてざっくりと以下を挙げておく
- コントローラがスッキリする
->Fat Controllerを避けられる - バリデーションロジックを再利用できる
->複数のコントローラで同じルールを使える - テストしやすくなる(今回の本題)
->FormRequestのrules()を直接テストできる
->コントローラを通さなくてよい
->バリデーションだけをピンポイントで検証できる
4.本題
簡単にFormRequestの概要を説明したところで今回の本題である、テストコードについてまとめていきます。
※筆者の環境
- Laravel:8.83.29
- PHP:8.1.34
- PHPUnit:9.6.31
- ホストOS:WIndows11
- WSLディストリビューション:Ubuntu22.04
- Dockerコンテナ:php:8.1-fpm
①FormRequestの作成
まず、ユーザー登録を行うAPIのFormRequestクラスを下記コマンドで作成し、各リクエスト項目に対しバリデーションを記載します。
php artisan make:request RegisterRequest
<?php
namespace App\Application\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
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:32',
'login_id' => 'required|string|max:32|unique:users',
'email' => 'required|string|email|max:255|unique:users',
'password' => [
'required',
'string',
Password::min(8)
->mixedCase()
->letters()
->numbers()
->symbols(),
'confirmed',
],
];
}
}
今回は上記のサンプルコードのテストコードを作成しました。
php artisan make:test RegisterRequestTest --unit
<?php
namespace Tests\Unit\Application\Http\Requests\Auth;
use Tests\TestCase;
use App\Application\Http\Requests\Auth\RegisterRequest;
use Illuminate\Support\Facades\Validator;
// docker compose run phpunit tests/Unit/Application/Http/Requests/Auth/RegisterRequestTest.php --testdox
class RegisterRequestTest extends TestCase
{
/**
* @param array $data
* @return \Illuminate\Validation\Validator
*/
private function validate(array $data)
{
$request = new RegisterRequest();
return Validator::make(
$data,
$request->rules(),
$request->messages(),
$request->attributes()
);
}
/**
* @test
* @dataProvider validDataProvider
*
* @param array $data
* @return void
*/
public function バリデーションが成功すること(array $data)
{
$validator = $this->validate($data);
$this->assertTrue($validator->passes());
}
public function validDataProvider()
{
return [
'valid data' => [
[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@exampole.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
],
];
}
/**
* @test
* @dataProvider invalidDataProvider
*
* @param array $data
* @param array $expected
* @return void
*/
public function バリデーションが失敗すること(array $data, array $expected)
{
$validator = $this->validate($data);
$this->assertTrue($validator->fails());
$this->assertSame($expected, $validator->errors()->toArray());
}
public function invalidDataProvider()
{
return [
'nameが空' => [
'data' =>[
'name' => '',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'name' => ['名前は必須です。'],
],
],
'nameが長すぎる' => [
'data' =>[
'name' => str_repeat('a', 33),
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'name' => ['名前は32文字以内で入力してください。'],
],
],
'login_idが空' => [
'data' =>[
'name' => 'Test User',
'login_id' => '',
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'login_id' => ['ログインIDは必須です。'],
],
],
'login_idが長すぎる' => [
'data' =>[
'name' => 'Test User',
'login_id' => str_repeat('a', 33),
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'login_id' => ['ログインIDは32文字以内で入力してください。'],
],
],
'emailが空' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => '',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'email' => ['メールアドレスは必須です。'],
],
],
'emailの形式が不正' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'invalid-email',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'email' => ['メールアドレスの形式が正しくありません。'],
],
],
'passwordが空' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => '',
'password_confirmation' => '',
],
'expected' => [
'password' => ['パスワードは必須です。'],
],
],
'passwordが短すぎる' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'sH0r@',
'password_confirmation' => 'sH0r@',
],
'expected' => [
'password' => ['パスワードは8文字以上で入力してください。'],
],
],
'passwordに数字が含まれていない' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'p@ssWord',
'password_confirmation' => 'p@ssWord',
],
'expected' => [
'password' => ['パスワードには少なくとも1つの数字を含めてください。'],
],
],
'passwordに英語小文字が含まれていない' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'P@SSW0RD',
'password_confirmation' => 'P@SSW0RD',
],
'expected' => [
'password' => ['パスワードには少なくとも大文字と小文字を1つずつ含めるてください。'],
],
],
'passwordに記号が含まれていない' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'PaSSW0RD',
'password_confirmation' => 'PaSSW0RD',
],
'expected' => [
'password' => ['パスワードには少なくとも1つの記号を含めてください。'],
],
],
'password確認が一致しない' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'different',
],
'expected' => [
'password' => ['パスワードが一致しません。'],
],
]
];
}
}
テストコードの解説
①validate()メソッド
まず、テストクラス内で定義している validate() メソッドはValidator を生成するための共通処理です。
private function validate(array $data)
{
$request = new RegisterRequest();
return Validator::make(
$data,
$request->rules(),
$request->messages(),
$request->attributes()
);
}
■ポイント
- FormRequest を new して rules() を直接呼び出す
→ コントローラを通さずにバリデーションだけをテストできる - messages() / attributes() も渡している
→ エラーメッセージや属性名の日本語化が反映される - Laravel8 の Password ルールは messages() を参照しないが、他の項目では正しく反映されるため渡しておくのがベスト
②バリデーション成功パターンのテスト
/**
* @test
* @dataProvider validDataProvider
*/
public function バリデーションが成功すること(array $data)
{
$validator = $this->validate($data);
$this->assertTrue($validator->passes());
}
■ポイント
- 成功パターンは
trueになることだけ確認すれば十分 -
dataproviderを使い、境界値等、複数パターンをテストしてれば安心
③バリデーション失敗パターンのテスト
/**
* @test
* @dataProvider invalidDataProvider
*/
public function バリデーションが失敗すること(array $data, array $expected)
{
$validator = $this->validate($data);
$this->assertTrue($validator->fails());
$this->assertSame($expected, $validator->errors()->toArray());
}
■ポイント
-
fails()がtrueであること(失敗してること)を確認 -
errors()->toArray()を使い、「項目名->エラーメッセージ配列」の形式で比較できる - 期待値との完全一致を確認し、翻訳漏れや誤字も併せて確認する
④失敗パターンのdataProvider
public function invalidDataProvider()
{
return [
'nameが空' => [
'data' =>[
'name' => '',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'name' => ['名前は必須です。'],
],
],
'nameが長すぎる' => [
'data' =>[
'name' => str_repeat('a', 33),
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'name' => ['名前は32文字以内で入力してください。'],
],
],
'login_idが空' => [
'data' =>[
'name' => 'Test User',
'login_id' => '',
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'login_id' => ['ログインIDは必須です。'],
],
],
'login_idが長すぎる' => [
'data' =>[
'name' => 'Test User',
'login_id' => str_repeat('a', 33),
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'login_id' => ['ログインIDは32文字以内で入力してください。'],
],
],
'emailが空' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => '',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'email' => ['メールアドレスは必須です。'],
],
],
'emailの形式が不正' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'invalid-email',
'password' => 'P@ssw0rd',
'password_confirmation' => 'P@ssw0rd',
],
'expected' => [
'email' => ['メールアドレスの形式が正しくありません。'],
],
],
'passwordが空' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => '',
'password_confirmation' => '',
],
'expected' => [
'password' => ['パスワードは必須です。'],
],
],
'passwordが短すぎる' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'sH0r@',
'password_confirmation' => 'sH0r@',
],
'expected' => [
'password' => ['パスワードは8文字以上で入力してください。'],
],
],
'passwordに数字が含まれていない' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'p@ssWord',
'password_confirmation' => 'p@ssWord',
],
'expected' => [
'password' => ['パスワードには少なくとも1つの数字を含めてください。'],
],
],
'passwordに英語小文字が含まれていない' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'P@SSW0RD',
'password_confirmation' => 'P@SSW0RD',
],
'expected' => [
'password' => ['パスワードには少なくとも大文字と小文字を1つずつ含めるてください。'],
],
],
'passwordに記号が含まれていない' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'PaSSW0RD',
'password_confirmation' => 'PaSSW0RD',
],
'expected' => [
'password' => ['パスワードには少なくとも1つの記号を含めてください。'],
],
],
'password確認が一致しない' => [
'data' =>[
'name' => 'Test User',
'login_id' => 'testuser',
'email' => 'test@example.com',
'password' => 'P@ssw0rd',
'password_confirmation' => 'different',
],
'expected' => [
'password' => ['パスワードが一致しません。'],
],
]
];
}
■ポイント
- dataにリクエストデータ、expectedにエラーメッセージを設定※失敗、成功は期待値に記載しなくてもテストできるためここでは含めていない
まとめ
- FormRequest は
rules()を直接呼び出せるためテストが書きやすい -
Validator::make()を使うことで FormRequest をユニットテストできる - エラーメッセージの一致まで確認することで、翻訳漏れも検知できる