はじめに
品質の高いWebアプリケーションを開発するためには、効果的なテスト戦略が不可欠です。単体テストやインテグレーションテストも重要ですが、実際のユーザー体験を確認するためには、エンドツーエンド(E2E)テストが必要になります。Laravel Duskは、Laravelが提供するブラウザテスト自動化ツールで、実際のユーザー操作をシミュレートし、アプリケーションの動作を包括的にテストすることができます。
この記事では、Laravel Duskの基本的な使い方から応用テクニックまで、実践的なコード例とともに解説します。
Laravel Duskとは
Laravel Duskは、ChromeDriverを利用してブラウザの操作を自動化し、ユーザーの実際の操作をシミュレートするテスト環境を提供します。以下のような特徴があります:
- JavaScriptを含む実際のブラウザ動作をテスト可能
- 複雑なUIインタラクションのテスト(ドラッグ&ドロップ、モーダル操作など)
- データベース操作と連携したE2Eテスト
- スクリーンショット撮影機能
- 簡潔で読みやすいAPI
インストールと初期設定
Duskのインストール
composer require --dev laravel/dusk
php artisan dusk:install
このコマンドを実行すると、以下の処理が行われます:
tests/Browser
ディレクトリの作成- 基本的なDuskテストファイルの作成
- ChromeDriverのダウンロード(環境に応じたバージョン)
環境設定
テスト用の環境変数を設定します。通常は.env.dusk.local
ファイルを作成し、テスト用の設定を記述します:
APP_URL=http://localhost:8000
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
ChromeDriverの管理
ChromeDriverのバージョンを更新するには:
php artisan dusk:chrome-driver
特定のバージョンを指定する場合:
php artisan dusk:chrome-driver 120
最初のDuskテストを作成
テストファイルの生成
php artisan dusk:make LoginTest
このコマンドでtests/Browser/LoginTest.php
ファイルが生成されます。
基本的なテストの記述
<?php
namespace Tests\Browser;
use Tests\DuskTestCase;
use Laravel\Dusk\Browser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class LoginTest extends DuskTestCase
{
use DatabaseMigrations;
/**
* ログイン機能のテスト
*/
public function test_user_can_login(): void
{
// テスト用ユーザーの作成
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password'),
]);
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('email', $user->email)
->type('password', 'password')
->press('ログイン')
->assertPathIs('/dashboard')
->assertSee('ダッシュボード');
});
}
}
テストの実行
テストを実行する前に、テスト用のWebサーバーを起動します:
php artisan serve
別のターミナルでテストを実行します:
php artisan dusk
特定のテストのみを実行する場合:
php artisan dusk tests/Browser/LoginTest.php
ブラウザの操作方法
基本的なナビゲーション
$browser->visit('/posts') // URLにアクセス
->clickLink('新規投稿') // リンクテキストをクリック
->click('@create-button') // セレクタを使用してクリック
->back() // ブラウザの「戻る」
->forward() // ブラウザの「進む」
->refresh() // ページの更新
->resize(1920, 1080); // ブラウザサイズの変更
フォーム操作
$browser->type('title', 'テスト投稿') // inputフィールドに入力
->select('category', '技術ブログ') // セレクトボックスの選択
->check('public') // チェックボックスをチェック
->uncheck('draft') // チェックボックスのチェックを外す
->radio('priority', 'high') // ラジオボタンを選択
->attach('file', __DIR__.'/test.jpg') // ファイルをアップロード
->press('投稿する') // ボタンをクリック
->submitForm('#post-form'); // フォームをサブミット
マウス操作と高度な操作
$browser->mouseover('@tooltip') // 要素にマウスオーバー
->drag('@item', '@target') // ドラッグ&ドロップ
->dragLeft('@slider', 100) // スライダーを左へ移動
->dragRight('@slider', 100) // スライダーを右へ移動
->pause(1000) // 1秒間待機
->keys('selector', '{enter}') // キーボード入力
->screenshot('filename') // スクリーンショット撮影
->script("window.scrollTo(0, 100)"); // JavaScriptを実行
複数のブラウザを使ったテスト
$this->browse(function (Browser $first, Browser $second) {
$first->loginAs(User::find(1))
->visit('/chat')
->waitFor('@chat-box');
$second->loginAs(User::find(2))
->visit('/chat')
->waitFor('@chat-box')
->type('@message', 'こんにちは!')
->press('@send');
$first->waitForText('こんにちは!')
->assertSee('こんにちは!');
});
アサーション(検証)
ページの検証
$browser->assertTitle('ダッシュボード') // タイトルの検証
->assertPathIs('/dashboard') // パスの検証
->assertPathBeginsWith('/admin') // パスの前方一致
->assertPathIsNot('/login') // パスの否定
->assertQueryStringHas('page', '2') // クエリ文字列の検証
->assertUrl('https://example.com/dashboard'); // 完全なURLの検証
テキストと要素の検証
$browser->assertSee('ようこそ') // テキストが表示されていることを確認
->assertDontSee('エラー') // テキストが表示されていないことを確認
->assertSeeIn('@header', 'ダッシュボード') // 特定要素内のテキストを確認
->assertPresent('@save-button') // 要素の存在を確認
->assertNotPresent('@delete-button') // 要素が存在しないことを確認
->assertVisible('@tooltip') // 要素が表示されていることを確認
->assertNotVisible('@hidden-menu') // 要素が非表示であることを確認
->assertEnabled('@submit') // 要素が有効であることを確認
->assertDisabled('@locked-input') // 要素が無効であることを確認
->assertChecked('@terms') // チェックボックスが選択されていることを確認
->assertNotChecked('@newsletter') // チェックボックスが選択されていないことを確認
->assertRadioSelected('priority', 'high') // ラジオボタンの選択を確認
->assertSelected('category', '技術ブログ'); // セレクトボックスの選択を確認
カウントと属性の検証
$browser->assertSourceHas('<h1>ダッシュボード</h1>') // ソースコードの検証
->assertSourceMissing('error-message') // ソースコードに含まれないことを確認
->assertAttribute('button', 'disabled', 'true') // 属性値の検証
->assertValue('input[name=email]', 'test@example.com') // input値の検証
->assertCount('@items', 5) // 要素数のカウント
->assertHasClass('@alert', 'alert-success') // クラスの存在確認
->assertClassMissing('@card', 'hidden'); // クラスの非存在確認
待機処理
基本的な待機
$browser->pause(1000) // 1秒間待機
->waitFor('@loading-indicator') // 要素が表示されるまで待機
->waitForText('処理が完了しました') // テキストが表示されるまで待機
->waitUntilMissing('@spinner') // 要素が消えるまで待機
->waitForTextMissing('読み込み中...'); // テキストが消えるまで待機
高度な待機
$browser->waitForReload() // ページのリロードを待機
->waitForLocation('/dashboard') // 特定のURLに移動するまで待機
->waitUntil('return !!window.jQuery') // JavaScriptの条件が真になるまで待機
->waitForVue('@counter', 'count', 5) // Vue.jsのプロパティが特定の値になるまで待機
->waitForDialog() // JavaScript確認ダイアログが表示されるまで待機
->waitForEvent('app-loaded'); // イベントが発生するまで待機
タイムアウトの指定
$browser->waitFor('@slow-element', 30) // 30秒間待機(デフォルトは5秒)
ページオブジェクトの活用
ページオブジェクトパターンを使用すると、テストコードを整理し、再利用性を高めることができます。
ページクラスの作成
php artisan dusk:page Dashboard
これにより、tests/Browser/Pages/Dashboard.php
が作成されます。
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Page as BasePage;
class Dashboard extends BasePage
{
/**
* このページのURL取得
*/
public function url()
{
return '/dashboard';
}
/**
* ページ上の要素のセレクタを定義
*/
public function elements()
{
return [
'@create-post' => '#create-post-button',
'@user-menu' => '.user-dropdown',
'@logout' => '.logout-button',
];
}
/**
* ページに特有の操作を定義
*/
public function createNewPost(Browser $browser, $title, $content)
{
$browser->click('@create-post')
->waitFor('#post-form')
->type('title', $title)
->type('content', $content)
->press('投稿する')
->waitForText('投稿が作成されました');
}
}
ページオブジェクトの使用
public function test_user_can_create_post()
{
$this->browse(function (Browser $browser) {
$browser->loginAs(User::find(1))
->visit(new Dashboard)
->createNewPost('テスト投稿', 'これはテスト投稿です。')
->assertPathIs('/posts');
});
}
コンポーネントの活用
繰り返し使用するUI要素をコンポーネントとして定義することもできます。
コンポーネントの作成
php artisan dusk:component Navbar
これにより、tests/Browser/Components/Navbar.php
が作成されます。
<?php
namespace Tests\Browser\Components;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Component as BaseComponent;
class Navbar extends BaseComponent
{
/**
* コンポーネントのルートセレクタを取得
*/
public function selector()
{
return '.main-navbar';
}
/**
* コンポーネントの要素のセレクタを定義
*/
public function elements()
{
return [
'@home' => 'a[href="/"]',
'@profile' => 'a[href="/profile"]',
'@settings' => '.settings-dropdown',
'@logout' => '.logout-link',
];
}
/**
* ユーザーをログアウトさせる
*/
public function logout(Browser $browser)
{
$browser->click('@settings')
->waitFor('@logout')
->click('@logout')
->waitForLocation('/login');
}
}
コンポーネントの使用
public function test_user_can_logout()
{
$this->browse(function (Browser $browser) {
$browser->loginAs(User::find(1))
->visit('/')
->within(new Navbar, function ($navbar) {
$navbar->logout();
})
->assertPathIs('/login');
});
}
データベースの準備
マイグレーションの実行
DatabaseMigrations
トレイトを使用して、各テスト前にマイグレーションを実行します:
use Illuminate\Foundation\Testing\DatabaseMigrations;
class PostTest extends DuskTestCase
{
use DatabaseMigrations;
// テストメソッド...
}
シーダーの実行
DatabaseSeeder
を実行してテスト用データを準備します:
public function setUp(): void
{
parent::setUp();
$this->seed();
}
テスト用ファクトリの活用
public function test_user_can_see_their_posts()
{
$user = User::factory()->create();
$posts = Post::factory()->count(3)->create(['user_id' => $user->id]);
$this->browse(function (Browser $browser) use ($user, $posts) {
$browser->loginAs($user)
->visit('/posts')
->assertSee($posts[0]->title)
->assertSee($posts[1]->title)
->assertSee($posts[2]->title);
});
}
認証テスト
ログイン状態のテスト
$browser->loginAs($user) // ユーザーとしてログイン
->visit('/profile')
->assertAuthenticated() // 認証状態を確認
ゲスト状態のテスト
$browser->visit('/profile')
->assertGuest() // 非認証状態を確認
->assertRedirect('/login');
CSRF保護のテスト
$browser->visit('/password/reset')
->assertPresent('input[name="_token"]');
高度なテクニック
スクリーンショットの活用
$browser->screenshot('login_page')
->press('ログイン')
->pause(1000)
->screenshot('after_login');
エラー時に自動的にスクリーンショットを取得するには、DuskTestCase.php
のtearDown
メソッドをカスタマイズします:
protected function tearDown(): void
{
$this->captureFailuresFor(function () {
parent::tearDown();
});
}
JavaScript例外のキャプチャ
$browser->withoutJavaScriptExceptions() // JS例外を無視
->visit('/buggy-page');
// または例外をキャッチしてテスト
$browser->assertVue('count', 5, '@counter')
->whenAvailable('.js-exception-indicator', function ($element) {
$this->fail('JavaScriptエラーが発生しました');
}, function () {
// エラーが発生しなかった場合の処理
});
ヘッドレスモードの使用
テストを高速化するために、ヘッドレスモードでテストを実行できます。tests/DuskTestCase.php
を編集します:
protected function driver()
{
$options = (new ChromeOptions)->addArguments([
'--headless',
'--window-size=1920,1080',
]);
return RemoteWebDriver::create(
'http://localhost:9515',
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
複数の環境でのテスト
DuskTestCase.php
を修正して、異なるブラウザサイズやデバイスでのテストを簡単に行えるようにします:
protected function driver()
{
$options = (new ChromeOptions)->addArguments([
'--window-size=1920,1080',
]);
if ($this->hasHeadlessDisabled()) {
$options->addArguments([
'--disable-gpu',
'--headless',
]);
}
// モバイルデバイスをエミュレート
if ($this->isMobile()) {
$options->setExperimentalOption('mobileEmulation', [
'deviceName' => 'iPhone X'
]);
}
return RemoteWebDriver::create(
'http://localhost:9515',
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
protected function isMobile()
{
return isset($_SERVER['DUSK_MOBILE']) && $_SERVER['DUSK_MOBILE'] === 'true';
}
protected function hasHeadlessDisabled()
{
return isset($_SERVER['DUSK_HEADLESS_DISABLED']) &&
$_SERVER['DUSK_HEADLESS_DISABLED'] === 'true';
}
CIでのDuskテスト
継続的インテグレーション(CI)環境でDuskテストを実行する方法も紹介します。
GitHub Actions設定例
.github/workflows/dusk.yml
:
name: Laravel Dusk Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
dusk-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, pdo_sqlite
- name: Copy .env
run: cp .env.example .env.dusk.local
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress
- name: Generate Key
run: php artisan key:generate --env=dusk.local
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Create Database
run: |
mkdir -p database
touch database/database.sqlite
- name: Update .env.dusk.local
run: |
echo "APP_URL=http://127.0.0.1:8000" >> .env.dusk.local
echo "DB_CONNECTION=sqlite" >> .env.dusk.local
echo "DB_DATABASE=database/database.sqlite" >> .env.dusk.local
- name: Run Migrations
run: php artisan migrate --env=dusk.local
- name: Install Chrome Driver
run: php artisan dusk:chrome-driver
- name: Start Chrome Driver
run: ./vendor/laravel/dusk/bin/chromedriver-linux &
- name: Run Laravel Server
run: php artisan serve --env=dusk.local &
- name: Run Dusk Tests
run: php artisan dusk --env=dusk.local
- name: Upload Screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: screenshots
path: tests/Browser/screenshots
テストコードの最適化
ベストプラクティス
- 適切なセレクタを使用する:IDやdusk属性を優先する
- 適切な待機を設定する:非同期処理のテストでは必ずwait系のメソッドを使用
- テストを独立させる:各テストは他のテストに依存しないようにする
- ページオブジェクトとコンポーネントを活用する:コードの重複を避ける
- テストデータを明示的に作成する:テストの意図を明確にする
テストの整理と管理
/**
* 認証機能に関するテストグループ
* @group auth
*/
public function test_something() { /* ... */ }
特定のグループだけを実行:
php artisan dusk --group=auth
トラブルシューティング
よくある問題と解決策
- 要素が見つからない場合:
- 待機時間を増やす:
$browser->waitFor('@element', 30)
- セレクタが正しいか確認する
- スクリーンショットを撮って状態を確認する
- 待機時間を増やす:
- CIでのタイムアウト:
- テストの前提条件を確認する
- ネットワーク負荷が高い操作は待機時間を長くする
- ブラウザのバージョン不一致:
- ChromeDriverのバージョンを更新する:
php artisan dusk:chrome-driver
- ChromeDriverのバージョンを更新する:
- セッション/Cookieの問題:
- テスト間でブラウザをクリアする:
$browser->visit('/logout')
または$browser->refresh()
- テスト間でブラウザをクリアする:
まとめ
Laravel Duskは、実際のユーザー操作をシミュレートしてE2Eテストを行うための強力なツールです。主な利点は以下の通りです:
- 実際のブラウザを使用した信頼性の高いテスト
- JavaScript動作の検証
- わかりやすいAPI
- スクリーンショット機能
- CIとの統合
適切にDuskテストを実装することで、リグレッションを早期に発見し、アプリケーションの品質を維持することができます。特に重要なユーザーフローや複雑なJavaScript操作を含む機能に対してE2Eテストを実装すると効果的です。
Laravelのテスト体系の中でDuskをうまく活用し、単体テスト、インテグレーションテストと組み合わせた総合的なテスト戦略を構築しましょう。
コメント