Pest4からブラウザテスト機能が追加され、簡単なセットアップだけでPlaywrightを使ったE2Eテストが使えるようになりました。今回はLaravelで作成したプロジェクトを使って、セットアップからテストコード作成までの流れを実際に試してみます。

セットアップ

私の環境ではユニットテストとしてすでにPestを導入済みですが、もしPHPUnit環境の方は移行に関する以下の記事もご参照ください。

LaravelのPHPUnitテストをpest-plugin-driftでPestへ変換

ブラウザテストはPest v4系で利用できるため、まずPestを更新します。

$ composer require pestphp/pest --dev --with-all-dependencies

v4.1.4に更新されました。次にブラウザテスト用のプラグインをインストールします。

$ composer require pestphp/pest-plugin-browser --dev

そして、Playwright本体とブラウザをインストールします。

$ npm install playwright@latest
$ npx playwright install

次に、スクリーンショットの保存ディレクトリをgitignoreに追記します。ドキュメントに記載の通り、テスト実行時に生成されるスクリーンショットをGit管理から外しておくためです。

保存先はブラウザテストのディレクトリによって変わります。私の環境ではtests/Browserにテストを配置するため、以下のようになります。

...
tests/Browser/Screenshots 

ここまでで準備は完了です。動作確認用に、画面遷移・表示確認だけの以下のようなシンプルなテストを作成しました。

it('example', function () {
    $page = visit('/');
    $page->assertSee('Laravel');
});

テストを実行します。テスト用のサーバーが自動で起動するので、php artisan serveは不要です。

$ ./vendor/bin/pest tests/Browser/BrowserTest.php

   FAIL  Tests\Browser\BrowserTest
  ⨯ it example                  0.04s  
  ────────────────────────────────────────────────  
   FAILED  Tests\Bro…  BindingResolutionException   
  Target class [config] does not exist.

テストが失敗してしまいました。新しく作成したBrowserディレクトリが、Pestの設定ファイルに追加されていないことが原因のようです。

Pest.phpを以下のように修正しました。Browserを追加しています。

pest()->extend(Tests\TestCase::class)
・・・
    ->in('Feature', 'Browser'); 

もう一度テストを実行してみます。

$  ./vendor/bin/pest tests/Browser/BrowserTest.php

   PASS  Tests\Browser\BrowserTest
  ✓ it example                  1.33s  

  Tests:    1 passed (1 assertions)
  Duration: 1.99s

テストが通りました!これで準備完了です。

新規作成フロー・テストコード全体

セットアップが整ったので、ここでは実際に作成したブラウザテストのコード全体を先にご紹介します。

テスト対象は前回も使用した英語日記アプリ新規作成機能です。

新規作成は3段階の画面遷移になっており、「日本語入力画面 → 英訳を入力・AI翻訳を実行 → 確認して保存」という流れになっているので、テストでは各画面でのテキスト入力や遷移の動作を確認しています。

また1日の入力文字数には制限があるので、入力したテキストの文字数によって文字数のカウント表示が正確に更新されているか、も確認しています。

use App\Models\User;

it('日記新規作成', function () {

    User::factory()->create([
        'email' => 'test@example.com',
        'password' => bcrypt('password'),
    ]);

    // ログインページへ移動
    $page = visit('/login');

    // メールアドレスとパスワードを入力
    $page->fill('email', 'test@example.com')
        ->fill('password', 'password')
        ->click('ログイン')
        ->assertPathIs('/user/diary');

    // 新規作成ページへ移動
    $page->click('a[href*="/diary/create"]')
        ->assertPathIs('/user/diary/create');

    // 入力テキストを準備
    $japaneseText = '今日は素晴らしい一日でした。';
    $englishText = 'Today was a wonderful day.';

    // 文字数カウンター確認用に、入力前の残り文字数を取得
    $initialRemaining =  (int)$page->text('#char_count');

    $inputLength = mb_strlen($japaneseText);
    $expectedRemaining = $initialRemaining - $inputLength;

   // 画面1: 日本語で入力
    $page->fill('native_content', $japaneseText)
        // 文字数カウンターの確認(計算した値と一致するか)
        ->assertSee("残り文字数: {$expectedRemaining}")
        ->click('次へ!');

    // 画面2: 英訳を入力
    $page->assertSee('英語で書いてみよう!')
        ->fill('user_translation', $englishText)
        ->click('翻訳する');

    // 翻訳中の画面表示が消えるまで待機
    $page->page()->getByText('翻訳中...')->first()->waitFor(['state' => 'hidden']);

    // 画面3への遷移待機
    $page->wait(1);

    // アサートに必要なデータの取得
    $aiTranslation = $page->value('translated_content');
    expect($aiTranslation)->not->toBeEmpty('AI翻訳結果が空です');

    // 画面3: 保存
    $page->click('保存')
        ->assertSee('日記が作成されました。')
        // 詳細画面での表示確認
        ->assertSee($aiTranslation)
        ->assertSee($japaneseText);
});

複数の画面遷移を含むのでコードは少し長めですが、メソッド名はどれもシンプルで分かりやすいので流れは追いやすいかと思います。

では続いて、今回のテストで使用したメソッドをまとめます。

画面遷移・操作系メソッド

まず、画面遷移にはvisit()を使用します。pageオブジェクトが返るので、後続のコードでは$pageを元に、画面を操作したりアサートしたりする形になります。

$page = visit('/login');

入力・クリック操作は、fill()click()を使用します。

$page->fill('email', 'test@example.com')
    ->fill('password', 'password')
    ->click('ログイン')

上記のコードでは、emailpasswordというname要素に指定の文字列を入力後、ログインボタンをクリックしています。

また指定の時間待機したい時はwait()を使用し、引数に秒数を渡します。

$page->wait(1);

要素の値を取得するメソッド

次に、要素の値を取得する時は、text()value()を使用します。

text()は、HTML要素内のテキストを取得します。以下の例では、idchar_countを持つ要素からテキストを取得しています。

$initialRemaining = $page->text('#char_count');

またvalue()は、inputtextareaselectなどフォーム要素のvalue値を取得します。以下のコードでは、idtranslated_contentという要素から文字列を取得しています。

$aiTranslation = $page->value('translated_content');

セレクタの指定について

ここまでの例で、セレクタの指定方法が複数混在していることに気づかれた方もいるかもしれません。

text('#char_count')のようにid明示するケースや、click('a[href*="/diary/create"]')のようにcssセレクタを直接指定する書き方、click('ログイン')のように文字列だけを渡しているケースなどがあります。

これはPestで定義されているGuessLocator機能のおかげです。GuessLocatorは、以下の優先順位で引数の情報から自動で要素を選択してくれます。

  1. #や.で始まる場合 → CSSセレクタとしてそのまま使用
  2. @で始まる場合 → data-testid/data-test属性として検索
  3. 通常の文字列の場合 → id、name属性として検索
  4. 上記で無い場合は文字列として要素を検索

こちらの詳細は、vendor/pestphp/pest-plugin-browser/src/Support/GuessLocator.phpに定義されています。

アサーション系

続いてアサーションです。URLの確認には、以下のようにassertPathIs()を使用します。

$page->click('ログイン')
    ->assertPathIs('/user/diary');

またassertSee()では、ページ内に引数のテキストが表示されているかを確認します。

$page->assertSee("日記が作成されました。");

使用可能なLaravelのメソッド

Pest4のブラウザテストでは、Laravelのテストヘルパーメソッドも使用できます。

先ほどのテスト内で使用しているfactory()だけでなくactingAs()assertAuthenticated()なども使えるので、ユーザーログインの手順をスキップしたい場合は以下のように簡略化できて便利です。

    $user = User::factory()->create([
・・・
    ]);

    $this->actingAs($user);
    $this->assertAuthenticated();
・・・

Playwrightのメソッドを使いたい場合

最後に、Pestのメソッドだけでは対応できない場合についてご紹介します。

今回のテストでは「特定の要素が非表示になるまで待機」という処理が必要でしたが、Pestの標準メソッドにはPlaywrightのwaitFor()に該当する機能がありませんでした。このような場合は、page()メソッドを使ってPlaywrightオブジェクトに直接アクセスできます。

    $page->page()->getByText('翻訳中...')->first()->waitFor(['state' => 'hidden']);

このコードでは、「翻訳中…」というテキストを持つ要素を取得し、その要素がhiddenになるまで待機させる処理を、Playwrightオブジェクトを経由して実行しています。

まとめ

テストコードの書き方自体は、CypressやPlaywrightを単体で使う場合と比べても大きな違いはないかなと思いました。

ただ今回のPest4はLaravelのテストとしてブラウザテストをプロジェクトに統合できるため、factoryでのテスト用レコードの生成や、認証ヘルパーなどがそのままE2Eテストに使用できます。その点が他と比べてもとても便利だと感じました。

メルマガ購読の申し込みはこちらから。

By hmatsu