まとめ
やりたいことは Laravel のバリデーションクラスであるフォームリクエストをユニットテストするためのノウハウ – oki2a24 の続きで、バリデーション時に更新対象ユーザーのメールアドレスはユニークルールから除外したい、更新対象ユーザーは、ルートのURLパスに指定している、ということです。
無事解決できました ! 本投稿の最下部に、コード全体を載せています。
環境
Laravel 6 ですが、以降のバージョンではどれでも今回の投稿は有効と思います。
app@2b58a9b2b8b4:/var/www/html/laravel$ php artisan -V
Laravel Framework 6.20.29
app@2b58a9b2b8b4:/var/www/html/laravel$
今回取り上げる例。テスト対象となるフォームリクエストクラスの rules
メソッド
PATCH or PUT api/user/{user}
というルートで使うフォームリクエストクラスとし、クラス名は UserUpdateRequest としました。
今回の投稿で取りあげたいのは、メールアドレスのバリデーションです。
メールフィールドを変更しない場合、 email の unique バリデーションルールは失格となります。そこで、 unique ルールを無視 するために ignore メソッドで対象ユーザーを無視します。無視する対象は、 PATCH or PUT api/user/{user}
の {user}
です。 ルートで定義されているURIパラメータにアクセスする ためには $this->route('user')
とします。
以上を踏まえ、 rules
メソッドは次のようになりました。
use Illuminate\Validation\Rule;
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|max:255',
'email' => [
'required',
'max:255',
'email:rfc,spoof',
Rule::unique('users')->ignore($this->route('user')),
],
'zip' => 'nullable|regex:/^\d{7}$/',
];
}
なお、クラスの生成は php artisan make:request UserUpdateRequest
を実行して行い、このテストクラスの生成も php artisan make:test UserUpdateRequestTest
を実行して行ました。
ユニットテストの何が難しいのか?
テストクラスを書く前に、この rules
をテストする時、何が難しいのかを明らかにします。
email のところに設けた unique ルールの ignore
メソッド内に書いた $this->route('user')
の値を取得できないため、難しいです。調べたところ、解決策は 2 つほどありそうでした。
- Mock を使う。
- フォームリクエストクラスをインスタンス化する時に setRouteResolver を使って routeResolver をセットすることで
route
メソッドを機能させる。
あと、これは自分で思いついた方法です。
- モデル結合ルート – 暗黙の結合 – ルーティング 6.x Laravel を使って、
$this->route('user')
を$this->user
に書き換える。テストでは、インスタンス化したフォームリクエストクラスに user プロパティをセットする。 - コントローラーも含めた範囲でテストする。
では実際にやってみます。
1. Mock を使う。
Mock の応用範囲は広いこと、 rules
メソッドに対する他のユニットテストのやり方とそんなに形を変えずに済むということ、という点がメリットかと思います。
次のコードで実現できました。ポイントを抜粋します。
//$request = new UserUpdateRequest();
$request = Mockery::mock(UserUpdateRequest::class)
->makePartial()
->shouldReceive('route')
->with('user')
->once()
->andReturn($user)
->getMock();
makePartial
メソッドを使うことにより $this->route('user')
のみを Mock にしました。 route
メソッドは親クラスのメソッドですけれども特別なことをする必要なく、 Mock にするクラスのメソッドとして扱うことができました。
2. フォームリクエストクラスをインスタンス化する時に setRouteResolver を使って routeResolver をセットすることで route
メソッドを機能させる。
リクエストクラスへの理解が深まる、より実際のコードの流れに近く、それでいて rules
メソッドのみをテストできている、という点がメリットかと思います。
次のコードで実現できました。
//$request = new UserUpdateRequest();
$request = UserUpdateRequest::create('api/user/'.$user->id, Request::METHOD_PATCH, $data);
$request->setRouteResolver(function () use ($request) {
return (new Route(Request::METHOD_PATCH, 'api/user/{user}', []))
->bind($request);
});
Symfony のリクエストクラス、 Laravel のルートクラスの理解が必要です。勉強をして理解を深めながら Laravel を使いたい場合は良いでしょうけれども、ただ単に使いたい場合には時間がただただすぎていくように感じるかもしれません。
- Simulating a Request – The HttpFoundation Component (Symfony Docs)
$this->route('user')
のroute
メソッド内では、getRouteResolver
を呼び出しており、getRouteResolver
の返す値を設定するのがsetRouteResolver
であるため、setRouteResolver
を使用する。- route – Illuminate\Http\Request | Laravel API
- __construct – Illuminate\Routing\Route | Laravel API
- bind – Illuminate\Routing\Route | Laravel API
3. モデル結合ルート – 暗黙の結合 – ルーティング 6.x Laravel を使って、 $this->route('user')
を $this->user
に書き換える。テストでは、インスタンス化したフォームリクエストクラスに user プロパティをセットする。
rules
メソッドに対する他のユニットテストのやり方とそんなに形を変えずに済むのに加え、 Mock も使わないのでよりコードがシンプルになる点がメリットと思います。
モデル結合ルート – 暗黙の結合 – ルーティング 6.x Laravel を読んでも、ルートに指定したモデルをリクエストのプロパティとしてアクセスできることは分かりません。しかし、 Larvel 8 の フォームリクエストの認可 – バリデーション 8.x Laravel を読むと
アプリケーションがルートモデル結合を利用している場合、解決されたモデルをリクエストのプロパティとしてアクセスすることで、コードをさらに簡潔にすることができます。
とあり、少なくとも Laravel 8 ではルートのモデルをリクエストのプロパティとして取得できることが分かりました。 Laravel 6 でももしかして、と思い、コードを修正後「フォームリクエストクラスをインスタンス化する時に setRouteResolver を使って routeResolver をセットすることで route
メソッドを機能させる。」 テストを実施してみたところテスト合格しましたので、 Laravel 6 でもルートに指定したモデルをリクエストのプロパティとしてアクセスできることが判明しました♪
ちなみに、このテストのための修正により、 Mock を使うテストは動かなくなる点に注意です。
コードは次のように修正しました。
laravel/app/Http/Requests/UserUpdateRequest.php
//Rule::unique('users')->ignore($this->route('user')),
Rule::unique('users')->ignore($this->user),
laravel/tests/Feature/UserUpdateRequestTest.php
$request = new UserUpdateRequest();
$request->user = $user;
$rules = $request->rules();
4. コントローラーも含めた範囲でテストする。
コントローラーも含めたコードでテストするので、本投稿の中で一番本番に近い状況となるメリットがあります。
つまり HTTP リクエストを作成し、出力をテストする HTTPテスト 6.x Laravel のやり方です。
レスポンスのステータスコードや、ボディを確認することで、テストします。
実際にコントローラークラスを実装済みであれば、有用なやり方ですけれども、実装していない場合 (本投稿を書いている現在、まさにそのような状況です!) はただただ面倒です (なのでコードの例示は省略したいと思います) 。
コード全体
書き殴った状態ですけれども、コード全体を掲載します。
- 「メール未変更でも正常終了すること1。Mock」テストを動かすには、テストのコメントを外し、フォームリクエストクラスの
rule
メソッドのRule::unique('users')->ignore
のコメントを入れ替えます。
laravel/app/Http/Requests/UserUpdateRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UserUpdateRequest 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|max:255',
'email' => [
'required',
'max:255',
'email:rfc,spoof',
//Rule::unique('users')->ignore($this->route('user')),
Rule::unique('users')->ignore($this->user),
],
'zip' => 'nullable|regex:/^\d{7}$/',
];
}
}
laravel/tests/Feature/UserUpdateRequestTest.php
<?php
namespace Tests\Feature;
use App\Http\Requests\UserUpdateRequest;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Validator;
use Mockery;
use Tests\TestCase;
class UserUpdateRequestTest extends TestCase
{
use RefreshDatabase;
/**
* @test
* @return void
*/
/*
public function メール未変更でも正常終了すること1。Mock(): void
{
$user = factory(User::class)->create();
$data = [
'name' => '名前',
'email' => $user->email,
'zip' => '0123456',
];
//$request = new UserUpdateRequest();
$request = Mockery::mock(UserUpdateRequest::class)
->makePartial()
->shouldReceive('route')
->with('user')
->once()
->andReturn($user)
->getMock();
$rules = $request->rules();
$validator = Validator::make($data, $rules);
$result = $validator->passes();
$this->assertTrue($result);
}
*/
/**
* @test
* @return void
*/
public function メール未変更でも正常終了すること2。setRouteResolver(): void
{
$user = factory(User::class)->create();
$data = [
'name' => '名前',
'email' => $user->email,
'zip' => '0123456',
];
//$request = new UserUpdateRequest();
$request = UserUpdateRequest::create('api/user/'.$user->id, Request::METHOD_PATCH, $data);
$request->setRouteResolver(function () use ($request) {
return (new Route(Request::METHOD_PATCH, 'api/user/{user}', []))
->bind($request);
});
$rules = $request->rules();
$validator = Validator::make($data, $rules);
$result = $validator->passes();
$this->assertTrue($result);
}
/**
* @test
* @return void
*/
public function メール未変更でも正常終了すること3。モデル結合ルート(): void
{
$user = factory(User::class)->create();
$data = [
'name' => '名前',
'email' => $user->email,
'zip' => '0123456',
];
$request = new UserUpdateRequest();
$request->user = $user;
$rules = $request->rules();
$validator = Validator::make($data, $rules);
$result = $validator->passes();
$this->assertTrue($result);
}
}
おわりに
フォームリクエストクラスの rule
メソッド内にルートで指定したモデルを使用している場合のテスト方法を 4 パターン紹介しました。どれがベストか、判断が難しいように思います。使い分けは状況に応じて都度判断となるかと思います。
Laravel の勉強であり知見を深める目的、本ブログのような目的、であるならば、フォームリクエストクラスをインスタンス化する時に setRouteResolver を使って routeResolver をセットすることで route
メソッドを機能させる、の方法がふさわしいと思いました。が、コントローラーも含めた範囲でのテスト機会は珍しそうなので除外することが多いでしょうけれども、 Mock でもルートに指定したモデルをリクエストのプロパティとしてアクセスできる暗黙の結合でも、別に構わないと今時点では感じています。
以上です。