php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

【現場完全ガイド】Laravelテスト戦略――Pest/PHPUnit、Feature/Unit、Factory、DB、HTTP/Queue/Notificationのモック、Dusk、アクセシブルUIの回帰テストまで

この記事で学べること(要点)

  • Laravelで「どこまでをテストするか」を迷わないための設計指針(Unit/Feature/E2Eの分担)
  • Factory/Seeder を使った“壊れにくい”データ準備と、テストの読みやすい書き方
  • 認証・認可・API・バリデーション・例外・キュー・通知・イベントの実務テストパターン
  • 外部API(HTTP)・時刻・ランダム・ストレージなど不安定要素の固定化(Fake/Mock)
  • CI(GitHub Actions 等)で速く回す工夫(並列・テスト分割・flaky対策)
  • Duskで画面を守る方法と、アクセシビリティ(フォーカス・エラー表示・ARIA)回帰テストの考え方

想定読者(だれが得をする?)

  • Laravel 初〜中級エンジニア:機能追加で壊れがちな箇所を、テストで守れるようになりたい方
  • テックリード/運用担当:CIで品質を担保し、レビューの負担と障害を減らしたい方
  • QA/デザイナー/アクセシビリティ担当:フォームや一覧の“操作できる体験”を継続的に守りたい方

アクセシビリティレベル:★★★★★

フォーカス移動、エラーサマリ、aria-invalidaria-describedbyrole="status"/alert を含むUIの回帰を、Duskや静的チェックで守る考え方を具体例つきで紹介します。


1. はじめに:テストは“安心して変更できる”状態を作るためにあります

Laravelの開発は速いです。だからこそ、変更も多くなります。変更が多いプロジェクトで一番つらいのは、「直したつもりで別の場所が壊れる」ことです。テストは、機能を増やすためのブレーキではなく、むしろアクセルです。安心して直せるから、改善もリファクタも進みます。

ただ、全部をテストしようとすると疲れてしまいます。大切なのは、テストを“書く順番”と“守るべき場所”を決めることです。この記事では、Laravelで現場に馴染みやすいテストの型を、Unit/Feature/E2E(Dusk)で整理し、さらにアクセシビリティの回帰まで視野に入れてまとめます。


2. まず決める:Unit/Feature/E2Eの役割分担

Unit(単体)

  • 小さなクラス(サービス、バリデーションルール、集計ロジック)を高速に検証
  • DBやHTTPに触れない(触れるならFeatureへ)

Feature(アプリの振る舞い)

  • ルート→ミドルウェア→コントローラ→DB→レスポンスまで
  • 認証/認可、バリデーション、例外、JSONレスポンスなどを“仕様”として固定
  • Laravelでは最も書きやすく、費用対効果が高いです

E2E(ブラウザ、Dusk)

  • UIの回帰を守る(フォーム、並び替え、ページング、アクセシブルなエラー表示など)
  • 過剰に増やすと遅くなるので、重要導線に絞るのが上手です

おすすめの比率(目安)

  • Feature:7
  • Unit:2
  • Dusk:1
    このくらいのバランスだと、速度と安心感が両立しやすいです。

3. テストの土台:Factoryと「読みやすいデータ作り」

テストが読みにくいと、メンテされずに腐っていきます。Factoryで“意図が見える”データを作るのがコツです。

3.1 Factory例(ユーザーとロール)

// database/factories/UserFactory.php
public function definition(): array
{
    return [
        'name' => $this->faker->name(),
        'email' => $this->faker->unique()->safeEmail(),
        'password' => bcrypt('StrongPassw0rd!'),
        'tenant_id' => Tenant::factory(),
    ];
}

public function admin(): static
{
    return $this->afterCreating(function (User $user) {
        $user->assignRole('admin');
    });
}

3.2 「状況が伝わる」名前を使う

テストの変数名は、短さより意味が大事です。

  • $user ではなく $adminUser
  • $p ではなく $otherTenantProject
    この工夫だけで、保守コストが下がります。

4. Featureテストの基本:HTTPとDBを“仕様として固定”する

4.1 認証が必要なページ

public function test_dashboard_requires_login()
{
    $res = $this->get('/dashboard');
    $res->assertRedirect('/login');
}

4.2 ログイン済みで正常表示

public function test_dashboard_shows_for_logged_in_user()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    $res = $this->get('/dashboard');
    $res->assertOk()->assertSee('ダッシュボード');
}

4.3 バリデーション(422)

public function test_store_rejects_invalid_input()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    $res = $this->post('/projects', ['name' => '']); // 必須違反
    $res->assertSessionHasErrors(['name']);
}

4.4 作成成功(PRGとDB)

public function test_store_creates_project()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    $res = $this->post('/projects', ['name' => '新規プロジェクト']);
    $res->assertRedirect('/projects');

    $this->assertDatabaseHas('projects', [
        'name' => '新規プロジェクト',
        'tenant_id' => $user->tenant_id,
    ]);
}

ポイント

  • 画面向けは assertSessionHasErrors が自然です。
  • API向けは postJson() と JSON構造の固定が強いです(後述します)。

5. 認可(Policy/Gate)を落とすテスト:境界漏れを防ぐ最重要ポイント

マルチテナントや権限周りは、事故が起きたときの影響が大きいので、Featureテストで守る価値が高いです。

public function test_user_cannot_update_other_tenant_project()
{
    $t1 = Tenant::factory()->create();
    $t2 = Tenant::factory()->create();

    $user = User::factory()->create(['tenant_id' => $t1->id]);
    $other = Project::factory()->create(['tenant_id' => $t2->id]);

    $this->actingAs($user);
    app()->instance('tenant', $t1);

    $res = $this->patch("/projects/{$other->id}", ['name' => '侵入']);
    $res->assertForbidden();
}

ここは少し丁寧にやりたいところで、

  • 403 を返すこと
  • DBが変化していないこと
    まで確認すると、さらに堅牢になります。

6. APIテスト:JSONレスポンスを“契約”にする

6.1 成功レスポンスの固定

public function test_api_returns_orders()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    Order::factory()->count(3)->create(['user_id' => $user->id]);

    $res = $this->getJson('/api/v1/orders');
    $res->assertOk()
        ->assertJsonStructure([
            'data' => [
                '*' => ['id','status','total']
            ],
            'meta' => ['page','per_page']
        ]);
}

6.2 エラー形式(problem+json)を固定

もし API を application/problem+json で統一しているなら、これが最強の守りになります。

public function test_api_validation_returns_problem_json()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    $res = $this->postJson('/api/v1/orders', ['items' => []]);
    $res->assertStatus(422)
        ->assertHeader('Content-Type', 'application/problem+json')
        ->assertJsonStructure(['type','title','status','detail','errors','trace_id']);
}

7. Fake/Mockで不安定要素を固定する(HTTP/通知/メール/イベント)

Laravelの良いところは、Fakeが揃っていて、実務テストが書きやすいことです。

7.1 外部API(HTTP)のFake

use Illuminate\Support\Facades\Http;

public function test_external_api_is_called()
{
    Http::fake([
        'api.example.com/*' => Http::response(['ok' => true], 200),
    ]);

    $res = $this->post('/sync');
    $res->assertRedirect();

    Http::assertSent(function ($req) {
        return str_contains($req->url(), 'api.example.com') && $req->method() === 'POST';
    });
}

7.2 通知(Notifications)

use Illuminate\Support\Facades\Notification;

public function test_notification_is_sent()
{
    Notification::fake();

    $user = User::factory()->create();
    $this->post('/password/reset', ['email' => $user->email]);

    Notification::assertSentTo($user, \App\Notifications\ResetPassword::class);
}

7.3 メール(Mail)

use Illuminate\Support\Facades\Mail;

public function test_invoice_mail_is_queued()
{
    Mail::fake();

    $user = User::factory()->create();
    $this->actingAs($user);

    $this->post('/invoices', ['plan' => 'pro']);

    Mail::assertQueued(\App\Mail\InvoiceCreated::class);
}

7.4 イベント(Event)

use Illuminate\Support\Facades\Event;

public function test_event_dispatched_on_create()
{
    Event::fake();

    $user = User::factory()->create();
    $this->actingAs($user);

    $this->post('/projects', ['name'=>'A']);

    Event::assertDispatched(\App\Events\ProjectCreated::class);
}

8. 時刻・乱数・キュー:再現性を守る小技

8.1 時刻固定(テストが安定します)

use Illuminate\Support\Carbon;

public function test_due_date_is_calculated()
{
    Carbon::setTestNow('2026-01-14 10:00:00');

    $due = app(DueDateCalculator::class)->forPlan('pro');
    $this->assertEquals('2026-02-14', $due->toDateString());
}

8.2 キュー(Queue)をFake

use Illuminate\Support\Facades\Queue;

public function test_job_dispatched()
{
    Queue::fake();

    $this->post('/export');

    Queue::assertPushed(\App\Jobs\ExportCsv::class);
}

9. DBテストを速く回す:RefreshDatabaseと「必要なところだけ重くする」

  • RefreshDatabase は読みやすい反面、DBリセットがコストになることもあります
  • 速さ重視なら、テストを分割し、重いケースだけDBを使う設計が効きます
use Illuminate\Foundation\Testing\RefreshDatabase;

class ProjectTest extends TestCase
{
    use RefreshDatabase;
}

また、Factoryで大量データを作るとテストが遅くなるので、

  • 必要最小限の件数
  • 集計は1〜3件
    を基本にするのがよいです。性能テストは別枠で行うと、通常のCIが軽くなります。

10. Duskで守るべきもの:重要導線とアクセシビリティ

Duskは強力ですが、増やすと遅いです。おすすめは、次の導線だけを厳選することです。

  • ログイン
  • 主要な作成フォーム(登録/購入/申請)
  • 一覧のフィルタ・ソート
  • 重要なエラー表示(バリデーションのサマリとフォーカス)

10.1 例:エラーサマリにフォーカスが当たる

public function test_error_summary_focuses_on_invalid_submit()
{
    $this->browse(function (\Laravel\Dusk\Browser $b) {
        $b->visit('/register')
          ->type('email', 'not-an-email')
          ->press('登録')
          ->assertPresent('#error-summary')
          ->assertFocused('#error-summary'); // フォーカスが当たっているか
    });
}

10.2 例:aria-invalid が付く

public function test_aria_invalid_set_on_error()
{
    $this->browse(function (\Laravel\Dusk\Browser $b) {
        $b->visit('/register')
          ->press('登録')
          ->assertAttribute('#email', 'aria-invalid', 'true');
    });
}

Duskでアクセシビリティの全てを網羅するのは現実的ではありませんが、

  • エラーサマリ
  • 必須入力のエラー
  • ローディング完了通知(role="status"
    このあたりは回帰が起きやすいので、守る価値が高いです。

11. “壊れないテスト”にするコツ:flaky対策と設計の小さな工夫

11.1 ランダムに依存しない

  • Fakerのランダム値に頼りすぎない
  • 固定の文字列や state() を使う
  • 期待値がブレないようにする

11.2 時間に依存しない

  • Carbon::setTestNow() を使い、期限判定を固定

11.3 Duskは待機を適切に

  • waitForTextwaitFor を使い、無理に pause() を増やさない
  • 画面の“完了条件”を明確に(例えば data-testid="loaded" を付けるなど)

12. CIで速く回す:最小構成の例と運用の考え方

12.1 速く回すための順序

  1. 静的解析(PHPStan/Pint)
  2. Unit/Feature
  3. Dusk(重要導線だけ)
  4. 余裕があれば契約テスト(OpenAPI差分)

12.2 テスト分割の考え方

  • 普段のPRは Unit/Feature のみ
  • メインブランチに入ったら Dusk も回す
  • 夜間に重いテスト(大量データ、性能)を回す
    こうしておくと、日中の開発速度が落ちにくいです。

13. テストに“サンプル”を残す:ドキュメントとしての価値

テストは仕様書でもあります。特に次のケースは、テストが最も読みやすい説明になります。

  • 認可の境界(誰が何をできるか)
  • エラー形式(APIの契約)
  • バリデーションの文言(入力UX)
  • 冪等性や二重送信(事故を防ぐ)

レビュー時に「仕様は?」と聞かれたら、テストを見せられる状態が理想です。


14. よくある落とし穴と回避策

  • 画面の文言に依存しすぎる(変更で大量に落ちる)
    • 回避:重要な文言だけを確認し、構造(ステータス、存在、ルール)を中心に
  • テストデータが巨大で遅い
    • 回避:必要最小限、集計は小さく、重いテストは別枠へ
  • 外部APIを実際に叩く
    • 回避:HTTP Fakeで固定、失敗パターンもFakeで作る
  • 認可テストがない
    • 回避:テナント境界・ロール境界はFeatureで必ず守る
  • Duskが不安定
    • 回避:待機条件を明確に、時間依存を排除、E2Eは厳選

15. チェックリスト(配布用)

テスト戦略

  • [ ] Unit/Feature/Dusk の役割と比率を決めた
  • [ ] 重要導線(登録/購入/申請/管理)をDuskで最小限守った

データ準備

  • [ ] Factoryが読みやすい(state/admin/tenantなど)
  • [ ] 変数名が意図を表している(adminUser など)

Feature(守るべき仕様)

  • [ ] 認証(未ログインはリダイレクト/401)
  • [ ] 認可(403、テナント境界)
  • [ ] バリデーション(422、エラー項目)
  • [ ] DB変更(assertDatabaseHas/Missing)

Fake/固定化

  • [ ] HTTP/Notification/Mail/Queue/Event を適切にFake
  • [ ] 時刻を Carbon::setTestNow() で固定

アクセシビリティ回帰

  • [ ] エラーサマリの存在とフォーカス(Dusk)
  • [ ] aria-invalid / aria-describedby の基本(Dusk)
  • [ ] 成功/進捗の role="status" が消えていない(必要な範囲で)

16. まとめ

Laravelのテストは、まずFeatureテストで“アプリの契約”を固めるのが近道です。認証・認可・バリデーション・APIレスポンスを仕様として固定し、外部要因はFakeで安定させます。Duskは重要導線に絞り、フォームのエラー表示やフォーカスといったアクセシビリティの回帰を守るのがおすすめです。テストが揃うと、変更が怖くなくなります。安心して改善できるプロジェクトは、結果として速く、そしてやさしく育っていきます。


参考リンク

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)