laravel9¦FormRequestのprepareForValidation,ルートモデルバインディングのunitテスト
やりたいこと
- FormRequestの
rules
でルートモデルバインディングしている値を使用する場合のテストもする -
prepareForValidation()
やpassedValidation()
もテストする
-
prepareForValidation()
でルートモデルバインディングしている値とリクエストの内容をかけ合わせたバリデーションのテストをする
環境
- Laravel 9
- php 8.1
- phpunit 9.5.21
ルートモデルバインディングしている値を使用している場合のテスト
patch
やput
などupdateする/staffs/{staff}
のようなルートの場合FormRequestでもstaffの情報にアクセスすることができ、取得したidなどをバリデーションのルールに追加することができます。
public function rules()
{
$staff = $this->route()->parameter('staff');
return [
'name' => 'required',
'name_kana' => 'required',
'email' => 'required|email|unique:staffs,email,' . $staff->id,
];
}
テスト以外の場合だとIlluminate\Routing\Route
のgetRouteResolver()
が呼ばれて意図した動作になるのですがunitテストの時はnullになります。悲しい ( ´~` )
そのためsetRouteResolver()
をセットする必要がありstackoverflowにあるような方法を使うと解決します。
use Illuminate\Routing\Route;
use Illuminate\Http\Request;
// 略
public function testSuccessValidation()
{
$params = $this->prepareSuccessParams();
foreach ($params as $param) {
$request = $this->executeRequest($this->staff);
$this->assertTrue(\Validator::make($param, $request->rules())->passes());
}
}
public function executeRequest(Staff $staff): StaffUpdateRequest
{
$request = new StaffUpdateRequest([], [], [], [], [], ['REQUEST_URI' => '/staffs/' . $staff->id]);
$request->setRouteResolver(function () use ($staff) {
$stub = $this->createStub(Route::class);
$stub->expects($this->any())->method('parameter')->with('staff')->willReturn($staff);
return $stub;
});
return $request;
}
FormRequest全体をテストする
laravelのunitテストでprepareForValidation()
やpassedValidation()
もテストする場合はFormRequest全体をテストする形になると思われます。
例えばprepareForValidation()
で郵便番号の内容を置換してからバリデーションする場合に、意図した置換になっているか確認する場合は↓このような感じになります。
public function prepareForValidation(): void
{
if (isset($this->postal_code)) {
$this->merge(['postal_code' => str_replace([' ', ' '], '', $this->postal_code)]);
$this->merge(['postal_code' => str_replace(['ー', '−', '-', '一'], '-', $this->postal_code)]);
$this->merge(['postal_code' => mb_convert_kana($this->postal_code, 'a'), $this->postal_code]);
}
}
/**
* @test
*/
public function testSuccessPrepareForValidationValidation()
{
$params = $this->prepareSuccessPrepareForValidationParams();
foreach ($params as $param) {
$result = $this->executeRequest($param);
$this->assertSame(1, preg_match('/\A\d{3}[-]\d{4}\z/', $result['postal_code']));
}
}
public function executeRequest(array $param): array
{
$request = \Request::create('/staffs/'. $this->staff->id, 'POST', $param);
app()->instance('request', $request);
$formRequest = app(StaffStoreRequest::class);
return $formRequest->all();
}
この書き方だとapp(StaffStoreRequest::class);
でバリデーションが通ってしまうためエラーのテストはできません。
エラーのテストもする場合はtry~catchでbool
を返す必要があります。
public function executeRequest(array $param): bool
{
$request = \Request::create('/staffs/'. $this->staff->id, 'POST', $param);
app()->instance('request', $request);
try {
app(StaffStoreRequest::class);
return true;
} catch (\Exception $e) {
return false;
}
}
ルートモデルバインディングしている値とリクエストの内容をかけ合わせたバリデーションのテストをする
先に載せたふたつの掛け合わせのテストをどう書くかという話なのですが、例えばprepareForValidation
でexistsのバリデーションを独自に書いたとして
public function prepareForValidation()
{
$info = $this->route()->parameter('info');
if (isset($this->assignStaffId) && $info->where('staff_id', $this->assignStaffId)->exists()) {
$this->merge(['assignStaffId' => intval($this->assignStaffId)]);
} else {
$this->request->remove('assignStaffId');
}
}
もしかしたら私の確認が甘かっただけかもしれませんが
最初に書いた$request = new StaffUpdateRequest([], [], [], [], [], ['REQUEST_URI' => ]);
のような方法でルートモデルバインディングしたら通るだろうと思ったらPATCH
にならずGET
になっていました。
最初の方法はルートモデルバインディングで落ちないようになっているのですがprepareForValidation
は通らないので、ルートモデルバインディングで落ちないようにしつつFormRequest全体をテストする必要がありました。
よ〜〜〜くコードを確認してみるとPATCH
にするには\Request::create
を使用しないと難しそうなので\Request::create
でPATCH
を指定し、最初に書いたような方法でsetRouteResolver
を定義すると意図した動作になりました。
public function executeRequest(array $param): bool
{
$request = \Request::create('/infos/' . $this->info->id, 'PATCH', $param);
$request->setRouteResolver(function () {
$stub = $this->createStub(Route::class);
$stub->expects($this->any())->method('parameter')->with('info')->willReturn($this->info);
return $stub;
});
app()->instance('request', $request);
try {
app(InfoUpdateRequest::class);
return true;
} catch (\Exception $e) {
return false;
}
}
🥟 🥟 🥟
冷静に考えればすぐわかるのかもですが
3つめのルートモデルバインディングしている値とリクエストの内容をかけ合わせたバリデーションのテストをするのにだいぶ苦戦しバリデーションの方を変えたい気持ちになったのですが(よくない)みなさんどのようにテストしているのだろう、、、( ´~` )
Discussion