Laravel

Laravel Duskでテスト自動化する方法|導入・書き方・CI実行【8.7.2実測】

Laravel Duskでテスト自動化する方法|導入・書き方・CI実行【8.7.2実測】

Laravelのテスト自動化で、画面をブラウザで操作して確かめる層を受け持つのが公式パッケージのLaravel Duskです。本記事では、Dusk 8.7.2をLaravel 13.39の新規プロジェクトに入れ、インストールからテストの書き方、待機処理、データベースの初期化、GitHub Actionsでの実行までを順に説明します。手順はmacOS(Intel)・PHP 8.5.8・Google Chrome 156で実行した結果に基づき、雛形のテストがそのままでは落ちることや、RefreshDatabaseを使うとテストが180秒止まることも、エラーメッセージ付きで示します。

まとめ:Laravel Duskのテスト自動化で押さえる点

  • Duskはブラウザ操作のE2Eテスト用。単体・機能テストは php artisan test、Duskは php artisan dusk で別々に実行する。
  • 導入は composer require laravel/dusk --dev と php artisan dusk:install。ChromeDriverは既定では最新安定版が入る。手元のChromeに合わせるには php artisan dusk:chrome-driver --detect を実行する。
  • Laravel 13の雛形では、dusk:install が置くサンプルの assertSee('Laravel') が失敗する。assertTitle などに書き換える。
  • データベースは DatabaseMigrations か DatabaseTruncation で初期化する。RefreshDatabase は公式が禁止しており、実測では失敗か180秒の停止になった。
  • JavaScriptで後から出る要素は waitForText などで待つ。待機の上限は既定で5秒。
  • 新規プロジェクトについて、公式ドキュメントはPest 4のブラウザテストを推奨している。Duskを選ぶのは既存のテスト資産がある場合が中心になる。

Laravelのテスト自動化におけるDuskの担当範囲

Laravel 13は、PestとPHPUnitによるテストを標準で使えます(Laravel 13.x Testing)。新規プロジェクトの composer.json にはPHPUnit 12.5(^12.5.12)が入っており、phpunit.xml のテストスイートは tests/Unit と tests/Feature の2つだけです。Duskのテストを置く tests/Browser はここに含まれないため、php artisan test を実行してもブラウザテストは動きません。

層 確かめる対象 実行コマンド ブラウザ
単体テスト クラス・メソッドの処理 php artisan test 使わない
機能テスト ルート・HTTPレスポンス php artisan test 使わない
ブラウザテスト(Dusk) JavaScriptを含む画面操作 php artisan dusk Chrome

機能テストはHTTPリクエストをアプリ内で模擬するだけなので、JavaScriptで描画される画面やモーダルの開閉は確かめられません。それをDuskが実際のChromeで補います。単体・機能テストの書き方はPestPHPとは?PHPUnitとの違い・導入手順と使い方|Pest 5.3を実測で解説、層ごとの本数の配分はテストピラミッドとは?単体・結合・E2Eの配分と実装者向けテスト戦略設計で扱っています。

DuskとSeleniumの関係も確認します。公式ドキュメントは、Duskの既定構成では手元にJDKもSeleniumも要らず、単体のChromeDriverを使うと説明しています。そのうえで、Selenium互換のドライバであれば別のものを使ってもよいとしています(Laravel 13.x Dusk)。生成される tests/DuskTestCase.php は環境変数 DUSK_DRIVER_URL があればその接続先を使い、無ければ http://localhost:9515 に接続します。Selenium Gridなどへ向けるときは、この変数で接続先を差し替えます。Chrome以外のブラウザを使う場合は、driver() が渡すChrome用の設定も書き換える必要があります。SeleniumそのものはPythonでの使い方をSeleniumとは?使い方をPythonで解説|4.51対応・ChromeDriverの手動設定が要らない書き方で解説しています。

Laravel Duskのインストール

前提はGoogle Chromeが入っていることです。Dusk 8.7.2(Packagist公開は2026年10月13日)の要求はPHP 8.1以上で、Laravel 10〜13(illuminate/console が ^10.0|^11.0|^12.0|^13.0)に対応します(Packagist laravel/dusk)。

composer require laravel/dusk --dev
php artisan dusk:install

手元では、composer require の結果が laravel/dusk v8.7.2(制約 ^8.7)と php-webdriver/webdriver 1.16.0でした。続く dusk:install は次の2行を出し、ChromeDriver 156.0.8078.80を入れました。

INFO  Dusk scaffolding installed successfully.
INFO  ChromeDriver binary successfully installed for version 156.0.8078.80.

dusk:install は --detect を付けずにChromeDriverを入れるため、入るのはその時点の最新安定版です。手元のChrome 156.0.8078.76とはメジャー版が一致していたので動きましたが、Chromeが古いままだと版がずれます。手元のChromeに合わせるときは php artisan dusk:chrome-driver --detect を実行します(ChromeDriverCommand.php)。

Duskは開発時の依存(--dev)として入れ、本番環境には入れません。公式ドキュメントは、サービスプロバイダを手で登録する場合も本番では登録しないよう警告しています。登録すると、任意の利用者がアプリにログインできてしまう恐れがあるためです。

dusk:installで作られるファイル

tests/DuskTestCase.php と、tests/Browser 配下の ExampleTest.php・Pages・Components・screenshots・console・source が作られます。DuskTestCase.php は、ポート9515でChromeDriverを起動し、ウィンドウを1920×1080にして、Chromeを --headless=new で開く設定です。ブラウザが画面に出ないので、CIでもこのまま動きます。画面を見ながら調べたいときは、環境変数 DUSK_HEADLESS_DISABLED を設定するとヘッドレスが外れます。

テストの接続先は .env の APP_URL です。公式ドキュメントは、ブラウザでアプリを開くときのURLと一致させるよう求めています。Laravel 13.39の .env.example は APP_URL=http://localhost:8000 なので、php artisan serve の既定ポート8000とそのまま合います。Duskは自分でWebサーバーを起動しません。テストの前に、別のターミナルで php artisan serve を起動しておきます。

雛形のExampleTestがLaravel 13で落ちる理由

dusk:install が置く ExampleTest.php は、トップページの本文に「Laravel」の文字があることを確かめる assertSee('Laravel') です。これをLaravel 13.39の雛形で実行すると、次のメッセージで失敗しました。

Did not see expected text [Laravel] within element [body].
Failed asserting that false is true.

このページで「Laravel」を含むのは <title>Laravel</title> だけでした。assertSee は body 要素の中しか探さないため、本文に該当がなく失敗します。導入直後に赤くなっても設定の誤りではありません。サンプルを次のように書き換えると、3.14秒で通りました。

public function test_basic_example(): void
{
    $this->browse(function (Browser $browser) {
        $browser->visit('/')
            ->assertTitle('Laravel');
    });
}

Duskテストの書き方と要素の指定

テストクラスは php artisan dusk:make LoginTest で tests/Browser に作ります。$this->browse() に渡したクロージャの中で、visit(ページを開く)、type(入力)、press(ボタン)、assertSee・assertSeeIn(表示の確認)をメソッドチェーンでつなげます。ログイン画面を毎回操作したくない場合は、loginAs でユーザーを指定すると、ログイン済みの状態から始められます。

以下の例は、認証済みユーザーがアクセスできる /dashboard を実装し、dusk="greeting" 属性を持つ要素に Hello Taro と表示する前提です。このルートと表示処理はLaravelの新規雛形には含まれないため、テスト対象側にも用意してください。

<?php

namespace Tests\Browser;

use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class DashboardTest extends DuskTestCase
{
    use DatabaseMigrations;

    public function test_dashboard_greets_user(): void
    {
        $user = User::factory()->create(['name' => 'Taro']);

        $this->browse(function (Browser $browser) use ($user) {
            $browser->loginAs($user)
                ->visit('/dashboard')
                ->assertSeeIn('@greeting', 'Hello Taro');
        });
    }
}

@greeting は、Blade側で <h1 dusk="greeting"> のように付けた dusk 属性を指します。CSSのクラス名はデザインの変更で変わりますが、dusk 属性はテスト専用なので変わりにくく、テストが壊れにくくなります。属性名を data-dusk に変えたいときは、AppServiceProvider の boot で Dusk::selectorHtmlAttribute('data-dusk') を呼びます。テストデータを作るFactoryの使い方はLaravel Seederの使い方|Factory連携と本番投入してよいデータの線引きを参照してください。

同じ画面を何本ものテストで操作するなら、php artisan dusk:page Login で作るページオブジェクトに、URLとセレクタの別名をまとめます。画面の変更が入ったときに直す場所が1か所で済みます。

RefreshDatabaseで止まる:データベースの初期化

機能テストで定番の RefreshDatabase は、Duskでは使えません。公式ドキュメントは、このトレイトはデータベーストランザクションを使っており、HTTPリクエストをまたいでは効かないため、Duskのテストでは決して使わないよう書いています(Resetting the Database After Each Test)。テストのPHPプロセスと、ブラウザが開く php artisan serve のプロセスは別物です。テスト側のトランザクションで作ったユーザーは、確定していないのでサーバー側からは見えません。

上の DashboardTest と同じ内容を、トレイトだけ替えてSQLiteファイル(database/database.sqlite)で実行した結果です。

トレイト セッション保存先 結果 所要時間
RefreshDatabase database(雛形の既定) タイムアウト 283.95秒
RefreshDatabase file 要素が見つからず失敗 9.58秒
DatabaseMigrations database 成功 4.32秒
DatabaseTruncation database 成功 4.05秒

Laravel 13の雛形はセッション・キャッシュ・キューの保存先がすべて database です。この構成で RefreshDatabase を使うと、テストは失敗せずに止まりました。_dusk/login/1 へのリクエストが返らず、WebDriverの Operation timed out after 180017 milliseconds で終わるまで約3分かかっています。セッションとキャッシュの保存先を両方 file にした実験ではタイムアウトせず、ログインできないまま no such element: Unable to locate element で失敗しました。CIが毎回数分止まる場合は、ブラウザテストに RefreshDatabase が紛れ込んでいないかを最初に確かめます。

代わりに使う2つのトレイトの違いは次のとおりです。

  • DatabaseMigrations:テストごとにマイグレーションを流し直す。テーブルの削除と作成を毎回行う。
  • DatabaseTruncation:最初のテストでマイグレーションし、以降はテーブルを空にするだけ。公式ドキュメントはこちらの方が速いとしている(既定で migrations テーブルは対象外)。

1本だけの計測では差は0.27秒でした。マイグレーションの再実行が重い構成では、DatabaseTruncation による短縮を期待できますが、今回の1本ずつの計測では本数やテーブル数による差までは確認していません。なお、phpunit.xml にある DB_DATABASE=:memory: はDuskには効きません。php artisan dusk は phpunit.dusk.xml が無ければ、環境変数の指定を持たない設定を一時的に作って実行し、.env のデータベースを使います。公式ドキュメントも、ブラウザは別プロセスなのでSQLiteのインメモリデータベースは使えないと明記しています。通常開発のデータを初期化しないよう、Duskには専用データベースを割り当てます。その接続設定を書いた .env.dusk.local をプロジェクト直下に置くと、実行中だけ .env と入れ替わり、終わると元に戻ります。

待機処理:JavaScriptで後から出る要素の扱い

Ajaxの応答や setTimeout で後から表示される要素は、ページを開いた直後に assertSee しても見つかりません。1.5秒後に「Saved」を表示するページで、3通りを試しました。

$browser->visit('/delayed')->assertSee('Saved');
$browser->visit('/delayed')->waitForText('Saved')->assertVisible('@done');
$browser->visit('/delayed')->waitFor('@never');
書き方 結果 所要時間
assertSee のみ 失敗(Did not see expected text [Saved]) 3.32秒
waitForText のあと assertVisible 成功 1.87秒
存在しない要素を waitFor TimeoutException 5.81秒

3つ目のメッセージは Waited 5 seconds for selector [@never]. でした。公式ドキュメントのとおり、waitFor の既定の上限は5秒で、第2引数で秒数を変えられます(waitFor('.selector', 1))。pause(2000) のような固定の待ちは、遅いCIでは足りず、速い環境では無駄になるので使いません。待つ対象に応じて次を使い分けます。

  • 要素の出現:waitFor、消えるのを待つなら waitUntilMissing
  • 文字の出現:waitForText、特定の要素内なら waitForTextIn
  • モーダルを待ってから中を操作:whenAvailable('.modal', function (Browser $modal) { ... })
  • ボタンを押して有効に戻るまで:pressAndWaitFor('Save')

テストの実行と失敗時の調べ方

php artisan dusk で tests/Browser の全テストが走ります。PHPUnitやPestに渡せる引数はそのまま使えるので、php artisan dusk --group=foo や、ファイルを指定して1本だけ実行することもできます。前回失敗したテストから先にやり直すには php artisan dusk:fails を使います。

失敗すると、その瞬間の画面が tests/Browser/screenshots に保存されます。手元では failure-Tests_Browser_WaitTest_test_assert_without_wait-0.png のように、「failure-」にクラス名とメソッド名を付けたファイル名でした。ブラウザのコンソール出力は tests/Browser/console に入ります(Storing Console Output to Disk)。原因が画面から読み取れないときは、ヘッドレスを外して目で追います。

ChromeDriverとChromeの版ずれ

Chromeが自動更新されると、入っているChromeDriverとメジャー版が合わなくなります。意図的にChromeDriver 150を入れてChrome 156で実行すると、次のエラーでテストが始まる前に止まりました。

session not created: This version of ChromeDriver only supports Chrome version 150
Current browser version is 156.0.8078.76

php artisan dusk:chrome-driver --detect を実行すると、手元のChromeの版を検出して156.0.8078.80に入れ直し、解消しました。dusk:chrome-driver 150 のように版の番号を渡すこともでき、--all を付けると全OS向けのドライバを入れます(Managing ChromeDriver Installations)。版ずれはCIでも起きるので、後述のワークフローでも毎回 --detect を実行します。

GitHub ActionsでのDusk実行設定

公式ドキュメントのGitHub Actionsの例は、APP_URL を http://127.0.0.1:8000 にし、ChromeDriverの更新と起動、php artisan serve --no-reload &、php artisan dusk の順に実行します(GitHub Actions)。ただし、この例はランナー上のMySQLにデータベースを作る前提です。Laravel 13の .env.example は DB_CONNECTION=sqlite なので、そのまま使うと作ったMySQLのデータベースは使われません。SQLiteのまま動かすなら、MySQLの手順を外した次の形になります。

name: dusk
on: [push]
jobs:
  dusk:
    runs-on: ubuntu-latest
    env:
      APP_URL: "http://127.0.0.1:8000"
      MAIL_MAILER: log
    steps:
      - uses: actions/checkout@v5
      - name: Set Up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.5'
          extensions: pdo_sqlite
          coverage: none
      - name: Prepare The Environment
        run: cp .env.example .env
      - name: Install Composer Dependencies
        run: composer install --no-progress --prefer-dist --optimize-autoloader
      - name: Generate Application Key
        run: php artisan key:generate
      - name: Upgrade Chrome Driver
        run: php artisan dusk:chrome-driver --detect
      - name: Start Chrome Driver
        run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=9515 &
      - name: Run Laravel Server
        run: php artisan serve --no-reload &
      - name: Run Dusk Tests
        run: php artisan dusk
      - name: Upload Screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: screenshots
          path: tests/Browser/screenshots

このワークフローは、公式の例からMySQLの作成手順と DB_USERNAME・DB_PASSWORD を外し、PHPを設定する手順を足したものです。GitHub Actions上では実行していません。SQLiteのファイル database.sqlite は database/.gitignore で除外されているため、チェックアウト直後には存在しません。手元でファイルを消してから DatabaseMigrations のテストを実行したところ、マイグレーションがファイルを作って成功しました。Chromeはランナーのイメージに入っているものを使います。PHPの設定は省けません。手元のPHP 8.5で作った composer.lock にはSymfony 8.1系が入り、PHP 8.4.1以上を要求します。一方、GitHubが公開しているUbuntu 24.04イメージの標準PHPは8.3.6です(Ubuntu 24.04 runner image)。そのため、Composerの前に shivammathur/setup-php で手元と同じ版を指定します。

失敗時だけスクリーンショットをアーティファクトとして上げておくと、CIで落ちた画面を後から確認できます。MySQLやRedisをサービスコンテナで立てる構成はGitHub Actionsのサービスコンテナ(services)|DB・Redisを立てる結合テストと起動待ちの設計、ワークフロー全般の書き方はGitHub Actionsでビルド・自動テストを設定する方法|build.ymlの書き方と失敗の直し方で扱っています。テスト自動化をCI/CDの整備ごと進めたい場合は、DevOps・CI/CD導入支援でもご相談を受け付けています。

DuskとPest 4ブラウザテストの選び方

Laravel 13.xのDuskのページ冒頭には、Pest 4にブラウザテストの機能が入り、Duskと比べて性能と使い勝手が大きく改善したと書かれています。そのうえで「For new projects, we recommend using Pest for browser testing.」とあり、新規プロジェクトにはPestを推奨しています(Laravel 13.x Dusk)。Pest 4のブラウザテストはChromeDriverではなくPlaywrightで動きます。

状況 選ぶもの
これからブラウザテストを書き始める Pest 4のブラウザテスト
Duskのテストが既に数十本ある Duskを維持し、新規分から検討
Selenium Gridや既存のWebDriver基盤に載せたい Dusk(DUSK_DRIVER_URL)
Node.jsやPlaywrightを入れられない環境 Dusk

新規プロジェクトで、Duskを第一候補にする理由はほとんどありません。公式ドキュメント自体が、新規にはPestを勧めているためです。一方、動いているDuskのテストを一度に書き換える必要もありません。両者を同じプロジェクトで併用する場合は、dusk:install が tests/Pest.php に tests/Browser を DuskTestCase で動かす設定を足すので、Pestのブラウザテストは別のディレクトリに置きます。導入手順とPlaywrightの版の注意点はPest 4のブラウザテストとは?pest-plugin-browserの導入とPlaywright版の落とし穴【4.7.8実測】にまとめています。

よくある質問

Laravel Duskとは何ですか?

Laravel公式の、ブラウザ操作の自動化とE2Eテストのためのパッケージです。既定ではChromeDriver経由でGoogle Chromeを動かし、クリックや入力、表示の確認をPHPのコードで書けます。2026年10月30日の確認時点で最新版は8.7.2で、Laravel 10〜13に対応します。

Laravel DuskにSeleniumは必要ですか?

既定の構成では不要です。DuskはChromeDriverを直接起動して使い、JDKもSeleniumも要りません。Selenium Gridなどを使う場合は、Seleniumサーバーを用意し、DUSK_DRIVER_URL で接続先を指定します。

php artisan testでDuskのテストは実行されますか?

実行されません。Laravel 13の phpunit.xml のテストスイートは tests/Unit と tests/Feature だけで、Duskの tests/Browser は含まれません。ブラウザテストは php artisan dusk で実行し、事前に php artisan serve を起動しておきます。

DuskのテストでRefreshDatabaseを使ってもよいですか?

使えません。トランザクションの中で作ったデータはブラウザ側のリクエストから見えず、Laravel 13の雛形の設定では、ログインのリクエストが応答しないまま約3分後にタイムアウトしました。DatabaseMigrations か DatabaseTruncation を使います。

DuskのテストはPestの書き方でも書けますか?

書けます。公式ドキュメントはPestとPHPUnitの両方の書き方を載せており、Pestでは pest()->use(DatabaseMigrations::class); のようにトレイトを指定します。Pest 4自体のブラウザテスト機能とは別物なので、どちらで動かすかを先に決めます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  2. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方
  3. 2026.10.31 テックブログ 無印良品の情報漏洩:MUJIアプリ最大2,726万件の対象確認となりすましメール対策
  4. 2026.10.31 テックブログ osinaの情報漏洩:約23万人の対象確認とSNS連携の解除、分析ツールへ渡すデータの絞り方
  5. 2026.05.18 テックブログ マネーフォワードで連携できない・失敗するときの対処法|エラー別の原因と再連携のやり方

RELATED POSTS 関連記事

目次