🦀

laravel9¦FormRequestのprepareForValidation,ルートモデルバインディングのunitテスト

2023/04/12に公開約5,400字

やりたいこと

  1. FormRequestのrulesでルートモデルバインディングしている値を使用する場合のテストもする
  2. prepareForValidation()passedValidation()もテストする
  1. prepareForValidation()でルートモデルバインディングしている値とリクエストの内容をかけ合わせたバリデーションのテストをする

環境

  • Laravel 9
  • php 8.1
  • phpunit 9.5.21

ルートモデルバインディングしている値を使用している場合のテスト

patchputなどupdateする/staffs/{staff}のようなルートの場合FormRequestでもstaffの情報にアクセスすることができ、取得したidなどをバリデーションのルールに追加することができます。

StaffUpdateRequest
public function rules()
{
    $staff = $this->route()->parameter('staff');
    return [
        'name' => 'required',
        'name_kana' => 'required',
        'email' => 'required|email|unique:staffs,email,' . $staff->id,
    ];
}

テスト以外の場合だとIlluminate\Routing\RoutegetRouteResolver()が呼ばれて意図した動作になるのですがunitテストの時はnullになります。悲しい ( ´~` )

そのためsetRouteResolver()をセットする必要がありstackoverflowにあるような方法を使うと解決します。

StaffUpdateRequestTest
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()で郵便番号の内容を置換してからバリデーションする場合に、意図した置換になっているか確認する場合は↓このような感じになります。

StaffStoreRequest
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]);
    }
}
StaffStoreRequestTest
/**
 * @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を返す必要があります。

StaffStoreRequestTest
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のバリデーションを独自に書いたとして

InfoUpdateRequest
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::createPATCHを指定し、最初に書いたような方法でsetRouteResolverを定義すると意図した動作になりました。

InfoUpdateRequestTest
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

ログインするとコメントできます