【現場完全ガイド】Laravelのテスト自動化とアクセシビリティ検証――Pest/PHPUnit・Feature/E2E・Dusk・axe/Pa11y・CI整備まで
この記事で学べること(要点)
- Pest/PHPUnit の使い分け、
Feature/Unit/Browser(Dusk)のテスト設計 - ファクトリ・シーディング・並列実行・モック/フェイク(Mail/Queue/Notification/Event)の実務パターン
- アクセシビリティの自動検査(axe-core/Pa11y)を Dusk/CI と連携する方法
- 画面遷移・フォーカス・ライブリージョン・キーボード操作のE2E検証
- カバレッジ・失敗スクリーンショット・テストデータ方針・高速化のコツ
- GitHub Actions を例にしたCI/CD パイプライン雛形
想定読者(だれが得をする?)
- Laravel 初〜中級エンジニア:テストの全体像を掴み、失敗に強いプロジェクトへ育てたい方
- テックリード/QA:E2Eとアクセシビリティ検査を毎プルリクで回したい方
- デザイナー/ライター:UI文言やエラーメッセージの読み上げ保証を仕組みに落とし込みたい方
アクセシビリティレベル:★★★★★
Dusk と axe/Pa11y を併用し、
role="status"/aria-live/aria-describedby/フォーカス移動/キーボード操作の達成確認まで自動化。失敗時の画面キャプチャとログ採取を CI に組み込みます。
1. はじめに:テストは「壊れにくさ」と「説明責任」を担保します
テストの目的は 仕様の固定化 と 安心して変更するための網 を用意することです。Laravel は HTTP テスト・データベースの巻き戻し・各種フェイク・ブラウザ操作(Dusk)を標準で提供します。ここにアクセシビリティ検査を重ねると、見える・読める・操作できるを継続的に保証できます。まず地図を描いてから、少しずつ整備していきましょう。
2. 全体設計:レイヤ別テストの役割
- Unit:小さな関数・クラスの純粋ロジック。I/O を避け、実行はミリ秒。
- Feature(HTTP/DB):ルート・バリデーション・認可・DB 反映・通知などユースケース。
- Browser(Dusk):本物のブラウザで画面操作、フォーカス・キー操作・読み上げを確認。
- A11y 自動検査:axe/Pa11y を Dusk/CI に統合し、基本的な欠陥を検出。
- 契約/スナップショット:API スキーマ(OpenAPI)や UI テキストの破壊的変更を検知。
現場では Feature 7割 / Unit 2割 / E2E 1割 程度の比率で始めると、コストに対して効果が出やすいです。
3. セットアップ:Pest・並列・DB・ファクトリ
3.1 Pest の導入(任意)
composer require pestphp/pest --dev
php artisan pest:install
tests/Featureとtests/Unitに 関数型テスト を書けます。PHPUnit と共存可能。
3.2 テスト用 DB と並列
php artisan test --parallel
# あるいは
php artisan test --parallel --processes=4
RefreshDatabaseトレイトでトランザクション巻き戻しまたはマイグレーション実行。- 並列時は in-memory SQLite か テスト用 MySQL の各プロセス分 DB を作成。
3.3 ファクトリと状態遷移
// database/factories/PostFactory.php
$factory->define(Post::class, fn() => [
'title' => fake()->sentence(),
'body' => fake()->paragraph(),
'status'=> 'draft',
'published_at' => null,
]);
// 状態
public function published(): static {
return $this->state(fn() => ['status'=>'published','published_at'=>now()]);
}
- **状態(state)**で下書き/公開などを明示。テストが読みやすくなります。
4. Feature テスト:ユースケースを固定化する
4.1 バリデーションと認可
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('記事を作成できる', function () {
$user = User::factory()->create();
$this->actingAs($user);
$res = $this->post('/posts', ['title'=>'はじめて','body'=>'本文']);
$res->assertRedirect('/posts');
$this->assertDatabaseHas('posts', ['title'=>'はじめて','user_id'=>$user->id]);
});
test('未ログインは作成できない', function () {
$this->post('/posts', ['title'=>'x','body'=>'y'])->assertRedirect('/login');
});
4.2 JSON API(Sanctum)
test('APIで一覧を取得できる', function () {
Sanctum::actingAs(User::factory()->create(), ['posts:read']);
Post::factory()->count(3)->published()->create();
$this->getJson('/api/v1/posts')
->assertOk()
->assertJsonCount(3, 'data')
->assertJsonStructure(['data'=>[['id','title','author'=>['id','name']]]]);
});
4.3 フェイクで副作用を検証
test('作成時に通知が送られる', function () {
Notification::fake();
$user = User::factory()->create();
$this->actingAs($user)->post('/posts', ['title'=>'x','body'=>'y']);
Notification::assertSentTo($user, \App\Notifications\PostPublished::class);
});
4.4 署名付きURL・レート制限
test('期限切れURLは拒否', function () {
$url = URL::temporarySignedRoute('files.show', now()->subMinute(), ['id'=>1]);
$this->get($url)->assertForbidden();
});
test('コメント投稿はスロットリング', function () {
RateLimiter::for('comment', fn()=>[Limit::perMinute(5)->by('t')]);
$this->actingAs(User::factory()->create());
for($i=0;$i<5;$i++){ $this->post('/comments', ['body'=>'hi']); }
$this->post('/comments', ['body'=>'hi'])->assertStatus(429);
});
5. ブラウザテスト(Dusk):操作と読み上げを“体感”で守る
5.1 セットアップ
php artisan dusk:install
php artisan dusk
- Chromedriver を同梱。CI では headless で実行。
5.2 基本の流れ
// tests/Browser/RegisterTest.php
public function test_register_flow()
{
$this->browse(function (Browser $b) {
$b->visit('/register')
->type('#name','山田花子')
->type('#email','hanako@example.com')
->type('#password','StrongPassw0rd!')
->type('#password_confirmation','StrongPassw0rd!')
->check('agree')
->press('登録する')
->waitForLocation('/dashboard')
->assertSee('ようこそ');
});
}
5.3 キーボード操作・フォーカス・ライブリージョン
public function test_error_summary_focus_and_readable()
{
$this->browse(function (Browser $b) {
$b->visit('/register')
->press('登録する') // 空送信
->waitFor('#error-title')
->assertFocused('#error-title') // 先頭サマリにフォーカス
->assertSeeIn('#error-title','入力内容を確認してください。')
->assertPresent('[role="alert"]')
->assertAttribute('#email','aria-invalid','true');
});
}
- エラー時は先頭サマリにフォーカスが移ること、
role="alert"の存在、各フィールドのaria-invalidを確認。 - 進捗や結果は
role="status"/aria-liveを読み上げ対象に。Dusk では DOM 上の有無で確認します。
5.4 画面キャプチャとログ
public function test_capture_on_failure()
{
$this->browse(function (Browser $b) {
$b->visit('/')->screenshot('home'); // storage/screenshots/home.png
});
}
- 失敗時自動キャプチャは デバッグ最短ルート。CI のアーティファクトに必ず保存しましょう。
6. 自動アクセシビリティ検査:axe/Pa11y を Dusk/CI と接続
6.1 Dusk × axe-core(最小統合例)
npm i -D axe-coreを導入。- Dusk から
axe.min.jsをページへ注入して評価します。
public function test_accessibility_smoke()
{
$this->browse(function (Browser $b) {
$b->visit('/register');
// axe を注入
$axe = file_get_contents(base_path('node_modules/axe-core/axe.min.js'));
$b->script($axe.'; void(0);');
// 実行して結果を受け取る
$results = $b->script('return axe.run(document, { resultTypes: ["violations"] });')[0];
// 重大度の高い違反を簡易検知
$violations = collect($results['violations'] ?? [])->filter(function($v){
return in_array('serious', $v['impact'] ?? []) || in_array('critical', $v['impact'] ?? []);
});
if ($violations->isNotEmpty()) {
logger()->error('axe.violations', ['violations'=>$violations]);
}
$this->assertTrue($violations->isEmpty(), 'アクセシビリティ違反があります');
});
}
- まずはスモーク(重大違反の検知)から。
- Detailed なルール調整は CI の Pa11y で行うと運用しやすいです。
6.2 Pa11y(CLI)でページ群を定期検査
npm i -D pa11y pa11y-cipa11y-ci.jsonを用意。
{
"defaults": {
"timeout": 30000,
"standard": "WCAG2AA",
"wait": 1000,
"chromeLaunchConfig": { "args": ["--no-sandbox","--disable-dev-shm-usage"] }
},
"urls": [
"http://localhost:8000/",
"http://localhost:8000/register",
"http://localhost:8000/login",
"http://localhost:8000/posts"
]
}
- CI で Laravel サーバを起動してから
npx pa11y-ciを実行。
- ページ追加はJSONに列挙。誤検知があれば ルール除外を個別に設定できます。
- ここで検出しきれない体験的な問題(フォーカス移動やライブ更新)は Dusk で補完します。
7. UIテキスト・エラー文・読み上げの検証
- エラー文は短く具体的、
aria-describedbyで入力と結び付け。 - トースト/バナーは優先度に応じて
role="status"/role="alert"を使い分け。 - 「色のみ」の表現がないか、テキスト/アイコンを併用しているかを Dusk で DOM 検査。
- 多言語化では
<html lang>と language-of-parts を確認(先行記事の方針を踏襲)。
8. テストデータ設計:偽装し過ぎず、現実に近く
- Factory の値は実在しそうな組み合わせに。住所/電話/価格はフォーマットを現実準拠で。
- Builder パターンで複雑な前提(例:公開済み記事+3件のタグ+2件のコメント)を簡潔に組み立てる。
- 大量データの性能検証は Seeder を使い、E2E では 固定ID で参照しやすく。
9. 高速化:ボトルネックを潰す
- 並列実行+ in-memory SQLite で起動速度を確保。
- 外部API は
Http::fake()でネットワーク不要に。 - Dusk はケースを絞って要点検証、重いシナリオは 1〜3本 に集約。
- フィード・一覧画面の E2E は API モックで安定化させ、UI の a11y を重点確認。
10. CI/CD:GitHub Actions 雛形(MySQL/Redis/Node 同時起動)
name: test
on: [push, pull_request]
jobs:
laravel:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8
env:
MYSQL_DATABASE: testing
MYSQL_USER: user
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root
ports: ["3306:3306"]
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -u root -proot"
--health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:alpine
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, pdo_mysql, intl
coverage: none
- name: Cache composer
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
- run: composer install --no-interaction --prefer-dist
- name: Setup Node
uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npm run build --if-present
- name: App key & env
run: |
cp .env.example .env
php artisan key:generate
php artisan migrate --force
- name: Pest / PHPUnit
run: php artisan test --parallel --processes=4
- name: Dusk (headless)
run: php artisan dusk --env=dusk.local
env:
APP_URL: http://127.0.0.1:8000
DB_DATABASE: testing
DB_USERNAME: user
DB_PASSWORD: secret
- name: Start server for Pa11y
run: php -S 127.0.0.1:8000 -t public & echo $! > server.pid && sleep 3
- name: Pa11y CI
run: npx pa11y-ci
- name: Upload artifacts (screenshots)
uses: actions/upload-artifact@v4
if: failure()
with:
name: dusk-screens
path: storage/screenshots
- 失敗時のスクリーンショットを必ずアップロード。
intlを有効化し、i18n/数値/日付のテストを通します。
11. よく使うフェイク/モックのレシピ
Mail::fake();
Queue::fake();
Event::fake();
Notification::fake();
Http::fake([
'https://api.example.com/*' => Http::response(['ok'=>true], 200),
]);
Storage::fake('s3'); // アップロード検証
- 「送ったつもり」を安全に検証。宛先・件名・件数のアサートを忘れずに。
- 画像/PDF の生成は ダミー で代替し、処理の有無だけ確認する方針が堅実です。
12. 画面の“読みやすさ”を守る E2E の観点
- フォーカスリングが消えていない(Tab で可視)。
- ボタン/リンクはキーボードだけで到達できる。
- 重要な通知は
role="alert"、通常はrole="status"、aria-liveはpoliteを基本。 - 画像に
alt、装飾は空alt=""。 - 動きの多い要素は
prefers-reduced-motionで軽くなる。 - エラーは色だけに依存せず、テキスト/アイコン/枠線で多重表現。
Dusk では DOM アサートを中心に、必要に応じてスタイルの 存在 も確認します(色値に依存しない)。
13. 代表的なサンプル(抜粋)
13.1 入力エラーの a11y
public function test_validation_errors_have_describedby()
{
$this->browse(function (Browser $b) {
$b->visit('/register')
->press('登録する')
->waitFor('#error-title')
->assertPresent('#email-error')
->assertAttribute('#email','aria-describedby', fn($v) => str_contains($v,'email-error'));
});
}
13.2 トースト読み上げ
public function test_toast_announces_status()
{
$this->browse(function (Browser $b) {
$b->visit('/profile')
->press('保存する')
->waitFor('[role="status"]')
->assertSeeIn('[role="status"]','保存しました');
});
}
13.3 代替テキスト
test('画像に代替テキストがある', function () {
$html = $this->get('/')->getContent();
expect($html)->toContain('alt=');
});
14. 落とし穴と回避策
- Dusk だけで全部やろうとする → 遅く不安定。Feature 中心で、体験の要点だけ Dusk。
- a11y 自動検査だけで安心 → フォーカス移動・ライブ更新は自動検知しづらい。E2E 補完を。
- テストデータが毎回ランダム → 失敗時に再現困難。シナリオは固定IDやシードで安定化。
- フェイクの戻し忘れ → 1テスト1アサートより前後処理の共通化(Pest のフックや PHPUnit の
setUp/tearDown)。 - CI での Chrome/DB 不整合 → ヘルスチェックを必ず入れる。
--disable-dev-shm-usageでメモリ不足回避。 - 失敗スクショ未保存 → 原因追跡が高コスト。アーティファクト保存を標準化。
15. チェックリスト(配布用)
戦略
- [ ] Feature(HTTP/DB)を中心、Unit はロジック、E2E は要点のみ
- [ ] 毎プルリクで Pest/Feature + Dusk + a11y(Pa11y/axe)を実行
データ
- [ ] Factory の状態設計(
draft/publishedなど) - [ ] シードと固定IDでシナリオを再現可能に
アクセシビリティ
- [ ] エラーのサマリにフォーカス、
role="alert"/aria-invalid/aria-describedby - [ ]
role="status"/aria-liveで進捗・完了を読み上げ - [ ] 画像
alt、装飾は空alt、色に依存しない表現 - [ ] キーボードで完了まで操作可能
自動検査
- [ ] Dusk + axe のスモーク
- [ ] Pa11y の URL リストを CI で回す
- [ ] 重大度ごとに失敗基準を定義
高速化/安定
- [ ] 並列実行・in-memory SQLite(または複数 DB)
- [ ] Http/Mail/Queue/Storage のフェイク
- [ ] 失敗時スクショ・ログをアーティファクト化
CI
- [ ] Chrome の headless 実行
- [ ] サービス(DB/Redis)ヘルスチェック
- [ ] 依存のキャッシュ(composer/npm)
16. まとめ
- Pest/PHPUnit・Feature・Dusk を役割分担させ、日々の変更を安全に。
- axe/Pa11y を取り入れて、見落としやすい a11y の初期欠陥を自動で抑止。
- フォーカス移動・ライブリージョン・キーボード操作など、体験の肝は E2E で確認。
- CI にスクリーンショットとログを残し、失敗から学べる体制に。
- まずはスモークから。効果を見ながら、ページとルールを段階的に広げていきましょう。
みなさまのプロジェクトが「壊れにくく、誰にでも使いやすい」状態で育っていくよう、この記事の雛形をそのままチームの標準にしていただけたら嬉しいです。
このコンテンツのアクセシビリティ評価:★★★★★(とても高い)
- a11y 自動検査と E2E で読み上げ・フォーカス・色非依存を継続的に担保。
- 失敗時の説明責任(スクショ/ログ/リクエストID)の運用まで含めました。
- 今後の強化:主要スクリーンリーダー(NVDA/JAWS/VoiceOver/TalkBack)での手動探索テストの定例化。
参考リンク
- Laravel 公式
- Pest / PHPUnit
- ブラウザ自動化・アクセシビリティ
- CI/ブラウザ実行
