0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【学習メモ】Laravelでupdate用FormRequestのバリデーション単体テストを考える

0
Last updated at Posted at 2026-06-28

Laravelでupdate用FormRequestのバリデーション単体テストを考える

はじめに

Laravelで登録処理のバリデーションをテストするときは、比較的シンプルに考えられます。

たとえば、タグ名を登録する場合、次のようなルールを書きます。

'name' => 'required|string|max:50|unique:tags,name',

これは、

tagsテーブルのnameに、同じ値が存在してはいけない

という意味です。

しかし、更新処理では少し考え方が変わります。

なぜなら、更新対象のデータは、すでにDBに存在しているからです。

この記事では、Laravelでupdate用FormRequestのバリデーション単体テストを行うときに、なぜRoute設定が必要になるのか、どのようにテストを組み立てるのかを整理します。


この記事で扱うこと


updateの単体テストが少し複雑になる理由

新規登録のバリデーションでは、次のように考えれば十分です。

tagsテーブルに同じnameが存在しないこと

たとえば、登録用Requestで次のようなルールを書いたとします。

'name' => 'required|string|max:50|unique:tags,name',

この場合、tags テーブルの name カラムに同じ値があれば、バリデーションエラーになります。

しかし、更新の場合は少し違います。

たとえば、DBに次のタグが存在しているとします。

id: 1
name: Laravel

このタグを更新するときに、名前を変更せず、そのまま Laravel として送信した場合は、本来エラーにしたくありません。

更新前: Laravel
更新後: Laravel

これは、自分自身の名前をそのまま維持しているだけだからです。

しかし、単純に次のルールだけを書くと問題が起きます。

'unique:tags,name'

このルールでは、Laravelは次のように判断します。

tagsテーブルに Laravel が存在している
↓
重複している
↓
バリデーションエラー

つまり、自分自身の名前なのに、重複エラーになってしまいます。

そのため、更新処理では次の考え方が必要です。

tagsテーブルのnameは一意であること
ただし、今更新している自分自身は重複チェックから除外する

更新用FormRequestのuniqueルール

更新用の TagUpdateRequest では、次のように書きます。

use Illuminate\Validation\Rule;

public function rules(): array
{
    $tag = $this->route('tag');

    return [
        'name' => [
            'required',
            'string',
            'max:50',
            Rule::unique('tags', 'name')->ignore($tag->id),
        ],
    ];
}

重要なのは、次の2つです。

$tag = $this->route('tag');
Rule::unique('tags', 'name')->ignore($tag->id)

$this->route('tag') の意味

$tag = $this->route('tag');

これは、

今更新しているタグをRouteから取得する

という意味です。

たとえば、ルートが次のようになっているとします。

Route::put('/tags/{tag}', [TagController::class, 'update'])
    ->name('tags.update');

この {tag} に対応するタグが、更新対象のタグです。

PUT /tags/1
↓
{tag} は id:1 のTag
↓
$this->route('tag') で取得できる

ignore($tag->id) の意味

Rule::unique('tags', 'name')->ignore($tag->id)

これは、次の意味です。

tagsテーブルのnameは一意であること
ただし、id が $tag->id のレコードは除外する

たとえば、次のデータがあるとします。

id: 1, name: Laravel

id:1のタグを更新するとき、ignore(1) と指定すると、重複チェックではid:1のレコードを除外します。

そのため、次の更新は通ります。

id:1 Laravel
↓
id:1 Laravel

一方で、別のタグにすでに使われている名前へ変更しようとすると、エラーになります。

id:1 name: Laravel
id:2 name: PHP

id:2 の PHP を Laravel に変更する
↓
Laravel は id:1 で使われている
↓
バリデーションエラー

FeatureテストとFormRequest単体テストの違い

更新処理のテストには、大きく分けて2つの考え方があります。

テスト 確認すること
Featureテスト 実際のルートを通して、更新処理全体が動くか
FormRequest単体テスト FormRequestのバリデーションルールだけが正しく動くか

Featureテストでは、次のように実際のルートを通ります。

$this->put(route('tags.update', $tag), [
    'name' => 'Laravel',
]);

この場合、Laravelが自動でRoute情報を扱ってくれます。

tags/{tag}
↓
{tag} に $tag が入る
↓
TagUpdateRequest内で $this->route('tag') が使える

一方、FormRequest単体テストでは、Requestを直接作ります。

$request = new TagUpdateRequest();

この場合、実際のURLやRouteを通っていません。

そのため、何もしないまま rules() を呼ぶと、Requestは「今どのタグを更新しているのか」を判断できません。


単体テストでRoute設定が必要な理由

更新用Requestでは、次のようにRouteから更新対象のタグを取得しています。

$tag = $this->route('tag');

しかし、単体テストでは実際のルートを通らずにRequestを直接作ります。

$request = new TagUpdateRequest();

このままだと、Requestの中で次の処理をしても、Laravelは更新対象のタグを判断できません。

$this->route('tag')

そこで、単体テスト側で仮のRouteを作り、Requestに設定します。

単体テストで仮のRouteを作る
↓
tags/{tag} の {tag} に更新対象のTagを設定する
↓
Requestに「このRouteを使ってください」と教える
↓
TagUpdateRequest内で $this->route('tag') が使える

これが、update用FormRequestの単体テストでRoute設定が必要になる理由です。


update用単体テストの基本形

TagUpdateRequest のバリデーション単体テストは、次のような形で書けます。

<?php

namespace Tests\Unit\Validation;

use App\Http\Requests\TagUpdateRequest;
use App\Models\Tag;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;

class TagUpdateRequestTest extends TestCase
{
    use RefreshDatabase;

    private function makeValidator(array $data, Tag $tag)
    {
        $request = new TagUpdateRequest();

        $route = new Route(['PUT'], 'tags/{tag}', []);
        $route->bind($request);
        $route->setParameter('tag', $tag);

        $request->setRouteResolver(function () use ($route) {
            return $route;
        });

        return Validator::make(
            $data,
            $request->rules(),
            $request->messages()
        );
    }

    private function validData(array $override = []): array
    {
        return array_merge([
            'name' => 'Laravel',
        ], $override);
    }
}

このテストは、実際にControllerのupdate処理を動かすものではありません。

確認しているのは、あくまで次の内容です。

TagUpdateRequestのrules()が、更新時の入力値を正しく判定できるか

makeValidatorの処理内容

ここでは、makeValidator() の中身を分解して見ていきます。


Requestオブジェクトを作る

$request = new TagUpdateRequest();

これは、TagUpdateRequestrules()messages() を使うために、Requestオブジェクトを作っています。

FormRequest単体テストでは、Controllerを通さずにRequestを直接作ります。


仮のRouteを作る

$route = new Route(['PUT'], 'tags/{tag}', []);

これは、単体テスト用に仮の更新Routeを作っています。

意味は次の通りです。

PUT tags/{tag}

['PUT'] は、このルートがPUTメソッド用であることを表します。

更新処理では通常 PUTPATCH を使うため、ここでは PUT を指定しています。


RouteとRequestを結びつける

$route->bind($request);

これは、

このRouteは、このRequestに対応しています

とLaravelに教える処理です。

この処理をしないままRouteパラメータを扱おうとすると、次のようなエラーになることがあります。

Route is not bound.

そのため、setParameter() を使う前に、RouteとRequestを結びつけておきます。


{tag} に更新対象のTagを設定する

$route->setParameter('tag', $tag);

これは、

tags/{tag} の {tag} に、更新対象の $tag を入れる

という意味です。

たとえば、次のタグを作ったとします。

$tag = Tag::create([
    'name' => 'PHP',
]);

この場合、仮Routeには次の情報が設定されます。

{tag} = id:1, name:PHP のTagモデル

これにより、Request内で次のように取得できます。

$this->route('tag')

RequestにRouteを教える

$request->setRouteResolver(function () use ($route) {
    return $route;
});

これは、Requestに対して、

route() が呼ばれたら、この仮Routeを返してください

と設定しています。

これにより、TagUpdateRequest の中で次のコードが動きます。

$tag = $this->route('tag');

処理の流れは次のようになります。

テスト側で $tag を作る
↓
仮Routeの {tag} に $tag を設定する
↓
RequestにそのRouteを設定する
↓
TagUpdateRequest内で $this->route('tag') を呼ぶ
↓
更新対象の $tag を取得できる

Validator::make() でバリデーションを実行する

return Validator::make(
    $data,
    $request->rules(),
    $request->messages()
);

ここでは、LaravelのValidatorを使って、FormRequestのルールを直接確認しています。

$data
↓
テストしたい入力値

$request->rules()
↓
TagUpdateRequestのバリデーションルール

$request->messages()
↓
TagUpdateRequestのエラーメッセージ

このようにすることで、ControllerやRoute全体を通さずに、FormRequestのバリデーションだけを確認できます。


validData() の役割

private function validData(array $override = []): array
{
    return array_merge([
        'name' => 'Laravel',
    ], $override);
}

validData() は、正常な入力データを作るための補助メソッドです。

$override を使うことで、一部の値だけを差し替えられます。

たとえば、タグ名を空にしたい場合は、次のように書けます。

$this->validData([
    'name' => '',
]);

これにより、毎回すべての入力データを書く必要がなくなります。


確認すべきテストケース

タグ更新のバリデーション単体テストでは、最低限次の項目を確認するとよいです。

1. タグ名を別の有効な名前に変更できる
2. タグ名が空の場合はエラーになる
3. タグ名は50文字まで通過する
4. タグ名が51文字以上ならエラーになる
5. 自身の名前維持は可能である
6. 他で既に使用されているタグ名への変更はエラーになる

特にupdateで重要なのは、次の2つです。

自身の名前維持は可能である
他で既に使用されている名前への変更は拒否する

タグ名を変更できるテスト

public function test_タグ名を変更できる(): void
{
    $tag = Tag::create([
        'name' => 'PHP',
    ]);

    $validator = $this->makeValidator(
        $this->validData([
            'name' => 'Laravel',
        ]),
        $tag
    );

    $this->assertFalse($validator->fails());
}

このテストでは、次の状況を作っています。

現在のタグ:
id:1, name:PHP

変更後の入力:
name:Laravel

DBには Laravel というタグが存在していません。

そのため、この入力は有効です。

このテストでは、

PHPタグをLaravelに変更する入力はバリデーションを通過する

ことを確認しています。


自身の名前維持は可能であるテスト

public function test_更新時に自身の名前維持は可能である(): void
{
    $tag = Tag::create([
        'name' => 'Laravel',
    ]);

    $validator = $this->makeValidator(
        $this->validData([
            'name' => 'Laravel',
        ]),
        $tag
    );

    $this->assertFalse($validator->fails());
}

このテストでは、次の状況を作っています。

現在のタグ:
id:1, name:Laravel

変更後の入力:
name:Laravel

普通の unique:tags,name だけなら、Laravel がDBに存在するため、エラーになります。

しかし、更新用Requestでは次のように書いています。

Rule::unique('tags', 'name')->ignore($tag->id)

これにより、自分自身のidは重複チェックから除外されます。

そのため、名前を変更せずに更新する入力はバリデーションを通過します。


他で使われている名前への変更は拒否するテスト

public function test_他で既に使用されているタグ名への変更はバリデーションエラーになる(): void
{
    $existingTag = Tag::create([
        'name' => 'Laravel',
    ]);

    $targetTag = Tag::create([
        'name' => 'PHP',
    ]);

    $validator = $this->makeValidator(
        $this->validData([
            'name' => $existingTag->name,
        ]),
        $targetTag
    );

    $this->assertTrue($validator->fails());

    $this->assertArrayHasKey(
        'name',
        $validator->errors()->toArray()
    );
}

このテストでは、DBに次の2件があります。

id:1, name:Laravel
id:2, name:PHP

そして、id:2 の PHPLaravel に変更しようとしています。

id:2 PHP
↓
id:2 Laravel

この場合、Laravel はすでに id:1 で使われています。

更新対象は id:2 なので、id:2 自身は重複チェックから除外されます。

しかし、id:1 の Laravel は除外されません。

そのため、バリデーションエラーになります。

このテストでは、次のことを確認しています。

他のタグで既に使われている名前には変更できない

空欄ならエラーになるテスト

public function test_タグ名が空の場合はバリデーションエラーになる(): void
{
    $tag = Tag::create([
        'name' => 'PHP',
    ]);

    $validator = $this->makeValidator(
        $this->validData([
            'name' => '',
        ]),
        $tag
    );

    $this->assertTrue($validator->fails());

    $this->assertArrayHasKey(
        'name',
        $validator->errors()->toArray()
    );
}

このテストでは、required が正しく動くかを確認しています。

nameが空
↓
requiredに違反
↓
バリデーションエラー

50文字までは通過するテスト

public function test_タグ名は50文字まで通過する(): void
{
    $tag = Tag::create([
        'name' => 'PHP',
    ]);

    $validator = $this->makeValidator(
        $this->validData([
            'name' => str_repeat('a', 50),
        ]),
        $tag
    );

    $this->assertFalse($validator->fails());
}

このテストでは、max:50 の境界値を確認しています。

50文字
↓
max:50以内
↓
通過

51文字以上ならエラーになるテスト

public function test_タグ名が51文字以上の場合はバリデーションエラーになる(): void
{
    $tag = Tag::create([
        'name' => 'PHP',
    ]);

    $validator = $this->makeValidator(
        $this->validData([
            'name' => str_repeat('a', 51),
        ]),
        $tag
    );

    $this->assertTrue($validator->fails());

    $this->assertArrayHasKey(
        'name',
        $validator->errors()->toArray()
    );
}

このテストでは、max:50 を超えた場合にエラーになることを確認しています。

51文字
↓
max:50を超える
↓
バリデーションエラー

よくある失敗

1. uniqueだけを書いてしまう

更新用Requestで次のように書くと、自分自身の名前も重複として扱われます。

'name' => 'required|string|max:50|unique:tags,name',

更新用では、自分自身を除外する必要があります。

Rule::unique('tags', 'name')->ignore($tag->id)

2. $this->route('tag') がnullになる

単体テストでRoute設定をしていないと、次の処理で更新対象のタグを取得できません。

$this->route('tag')

その結果、次のようなエラーになることがあります。

Attempt to read property "id" on null

単体テストでは、仮Routeを作り、Requestに設定する必要があります。

$route = new Route(['PUT'], 'tags/{tag}', []);
$route->bind($request);
$route->setParameter('tag', $tag);

$request->setRouteResolver(function () use ($route) {
    return $route;
});

3. Route is not bound. が出る

次のようなエラーが出る場合があります。

Route is not bound.

これは、RouteとRequestが正しく結びついていない状態で、Routeパラメータを扱おうとしている可能性があります。

その場合は、次の処理があるか確認します。

$route->bind($request);

4. assertFalseとassertTrueを逆にしてしまう

バリデーションの成功・失敗を確認するときは、fails() の意味に注意します。

$validator->fails()

これは、

バリデーションに失敗したか

を返します。

そのため、通過してほしい場合は次のように書きます。

$this->assertFalse($validator->fails());

エラーになってほしい場合は、次のように書きます。

$this->assertTrue($validator->fails());

5. テストの目的がDB更新確認になってしまう

FormRequest単体テストで確認するのは、DBの値が更新されたかどうかではありません。

確認するのは、入力値がバリデーションを通過するか、エラーになるかです。

FormRequest単体テスト
↓
rules() が正しく判定するかを確認する

Featureテスト
↓
実際に更新処理が成功するかを確認する

この2つを分けて考えることが大切です。


逆引き表

やりたいこと 書き方
更新対象のタグを取得する $this->route('tag')
自分自身をuniqueチェックから除外する Rule::unique('tags', 'name')->ignore($tag->id)
FormRequestを直接作る $request = new TagUpdateRequest();
仮のPUTルートを作る new Route(['PUT'], 'tags/{tag}', [])
RouteとRequestを結びつける $route->bind($request)
{tag} に更新対象を設定する $route->setParameter('tag', $tag)
Requestに仮Routeを設定する $request->setRouteResolver(...)
バリデーションを実行する Validator::make($data, $request->rules(), $request->messages())
通過することを確認する $this->assertFalse($validator->fails())
エラーになることを確認する $this->assertTrue($validator->fails())
エラー項目を確認する $this->assertArrayHasKey('name', $validator->errors()->toArray())

まとめ

update用FormRequestの単体テストが少し複雑になる理由は、更新対象のモデルをRequest内で使う必要があるからです。

更新用Requestでは、次のように現在更新しているタグをRouteから取得します。

$tag = $this->route('tag');

そして、次のように自分自身を重複チェックから除外します。

Rule::unique('tags', 'name')->ignore($tag->id)

Featureテストであれば、実際のルートを通るため、LaravelがRoute情報を自動で扱ってくれます。

しかし、FormRequest単体テストではRequestを直接作るため、テスト側で仮のRouteを作って設定する必要があります。

$route = new Route(['PUT'], 'tags/{tag}', []);
$route->bind($request);
$route->setParameter('tag', $tag);

$request->setRouteResolver(function () use ($route) {
    return $route;
});

この処理は、簡単に言うと次の意味です。

この単体テストでは、$tag を「今更新しているタグ」として扱ってください

update用FormRequestの単体テストでは、特に次の2つを確認することが重要です。

自分自身の名前はそのまま使える
他のレコードで使われている名前には変更できない

この2つを確認できれば、更新時の unique バリデーションの考え方を正しくテストできます。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?