jest-puppeteer を使用したエンドツーエンドテストの簡単ガイド
この記事では、jest-puppeteer を使用した効率的なエンドツーエンドテストを書くための簡単なガイドを提供します。セットアッププロセス、よく使われる API、そして簡単な to-do アプリを例に用いた実際のテストシナリオに焦点を当てています。
Logto の品質を確保し、継続的に改善するための取り組みの一環として、エンドツーエンドの自動テストには jest-puppeteer を採用しています。これにより、干渉を受けずに Logto の開発を迅速に反復できるようになります。
この記事では、簡単な to-do アプリを例に取りながら、効果的な jest-puppeteer テストスクリプトを書くための経験を共有します。目的は、jest-puppeteer を使用したテストコードを自身のプロジェクトで迅速に書き始めるのを手助けすることです。
jest-puppeteer を使用したエンドツーエンドテストの紹介
エンドツーエンドテストは、ユーザーの視点からアプリケーションが正しく動作していることを確認するための方法です。これを実現するために、Jest と Puppeteer という 2 つの基本的なツールを使用します。
Jest は、テストとアサーションを記述するためのユーザーフレンドリーな API を提供する、人気の JavaScript テストフレームワークです。ユニットテストや統合テストに広く使用されています。
Puppeteer は Chrome チームによって開発された Node.js ライブラリで、ヘッドレス Chrome または Chromium ブラウザを制御するためのハイレベルな API を提供します。そのため、エンドツーエンドテストでのブラウザ操作を自動化するのに理想的な選択です。
jest-puppeteer は、Puppeteer を使用したエンドツーエンドテストを可能にする Jest のプリセットです。新しいブラウザインスタンスを起動し、それらを通じてウェブページと対話するための簡単な API を提供します。
基本的なツールの理解ができたところで、テスト環境のセットアップに進みましょう。
テスト環境のセットアップ
jest-puppeteer を使用したエンドツーエンドテストのテスト環境のセットアップは、簡単な 3 つのステップで構成されています。
- 依存関係のインストール
プロジェクト内(または新しいプロジェクトを作成して)ターミナルを開き、次のコマンドを実行します:
次に node_modules/puppeteer
に移動し、Puppeteer に必要な Chromium をインストールします:
- Jest の設定
次に、Jest を Puppeteer とシームレスに動作させるために設定する必要があります。
プロジェクトのルートディレクトリに Jest 設定ファイル(例: jest.config.js
)を作成します。
必要に応じてこのファイルで他の Jest 設定をカスタマイズすることができます。Jest 設定のカスタマイズの詳細は、Jest 設定 を参照してください。
- テストを書く
プロジェクト内に .test.js
拡張子のテストファイルを作成します。Jest はこれらのテストファイルを自動的に検出して実行します。
Jest のドキュメント からの例は以下の通りです:
次にテストを実行するためのコマンドを入力します:
これらの 3 ステップを踏むことで、jest-puppeteer を使用したエンドツーエンドテストを実施するための適切なテスト環境が整います。
ただし、これは基本的な例に過ぎません。環境設定の詳細情報については、以下の関連文書を参照してください:
よく使われる API
次のステップでは、テストを支援するために Jest、Puppeteer、jest-puppeteer が提供する API を利用します。
主に、Jest はテストの構造化と予期した結果のアサーションに役立つ API を提供します。具体的な詳細は ドキュメント にて参照できます。
Puppeteer の API は主にブラウザとの対話のために設計されており、それらが提供するテストに対するサポートはそれほど単純ではないかもしれません。Puppeteer が提供する機能を理解するために、Puppeteer ドキュメント を参照できます。次のテスト例では、いくつかの一般的な使用例について説明します。
Puppeteer の API は元々テスト用に設計されていないため、それらを使ってテストを書くには挑戦が必要です。Puppeteer テストを書くプロセスを簡略化するために、jest-puppeteer には expect-puppeteer
という組み込みライブラリが含まれています。これは、簡潔でユーザーフレンドリーな API のセットを提供します。ライブラリーの詳細は、各例を示す ドキュメント にて紹介されています:
次の例では、これらのライブラリーが提供する API を組み合わせてテストシナリオを完成させます。
では、簡単な to-do アプリを使ってテストコードの書き方を学び始めましょう。
簡単な to-do アプリ
「簡単な to-do アプリ」が http://localhost:3000
で動作していると仮定します。このアプリの主な HTML コードは次のとおりです:
エンドツーエンドテストを行う際、次のシナリオをカバーすることを目指しています:
- アプリの URL にアクセスする際、アプリとそのデータが正しく読み込まれるべきです。
- 項目のチェックボタンがクリックできるべきです。
- 項目ノートの内部にある外部リンクをクリックすると、新しいタブでリンクが開かれるべきです。
- フォームから項目を追加できるべきです。
後で、これらのシナリオを検証するためのテストコードを書きます。
アプリとそのデータが読み込まれたと予期すること
アプリが次の条件を満たしている場合、正常にロードされていると考えています:
- アプリの URL にアクセスした後、ページに ID "app" を持つ要素が存在する。
- アプリ内部のデータが正しくレンダリングされる。
そのため、次のテストコードを記述しました:
このコードでは:
page.goto
はブラウザで "http://localhost:3000" に入力するのと同じで、任意の URL にナビゲートできます。page.waitForSelector
は特定の CSS セレクターが特定の要素にマッチするのを待つために使用します(デフォルトの待ち時間は 30 秒です)。expect(page).toMatchElement
はexpect-puppeteer
ライブラリから来ており、page.waitForSelector
に似ていますが、要素の内部テキストもマッチングするサポートが含まれています。さらに、toMatchElement
のデフォルトタイムアウトはわずか 500ms です。
一見完璧に見えますが、テストを実行して CI 環境にデプロイすると、複数回の実行後に稀に失敗します。失敗メッセージには次のように記されています:
失敗情報とこのテストが大半の時間で成功する事実に基づいて、アプリが読み込まれた後の最初の 500ms 以内にデータが必ずしも返ってくるとは限らないと推測できます。そのため、アサーションを行う前にデータが読み込まれるまで待ちたいと考えます。
通常、この目的を達成するためのアプローチは 2 つあります:
- アサーションの待ち時間を増やす
エラーメッセージからわかるように、toMatchElement
のデフォルトの待ち時間は 500ms に設定されています。この関数に timeout
オプションを追加して待ち時間を増やすことができます:
このアプローチは失敗テストの発生をある程度減らせるかもしれませんが、データが取得されるまでに必要な時間が不明であるため、問題を完全に解 決するわけではありません。
したがって、一定の待ち時間が必要だと確信しているシナリオ、例えば「要素に 2 秒以上マウスをホバーするとツールチップが表示される」などのシナリオにのみ、このアプローチを使用します。
- アサーションを行う前にネットワークリクエストの完了を待つ
これは正しいアプローチです。データリクエストにどれだけの時間がかかるかはわからないかもしれませんが、ネットワークリクエストの終了を待つのは常に安全な選択です。この時点で page.waitForNavigation({ waitUntil: 'networkidle0' })
を使用して、ネットワークリクエストが完了するのを待つことができます:
このようにして、アプリとその読み込まれたデータに対するアサーションを実行する前に、ネットワークリクエストが既に終了していることを確実にできます。これによりテストが常に正しい結果を生み出すことが保証されます。
特定のボタンがクリックされることを期待する
次に、項目内部のチェックボタンがクリックされる機能をテストします。
例のアプリでは、すべての項目が同じ構造を持っていることがわかりました。それらのチェックボタンには、li[class$=item] > button
という CSS セレクターがあり、ボタンのテキストは常に "Check" です。したがっ て、どの項目のチェックボタンをクリックするかを直接指定することができません。それで、新しい解決策が必要です。
たとえば、「Read Logto get-started document」項目のチェックボタンをクリックする場合、それを 2 つのステップに分解できます:
- まず、その特定の項目への参照を取得します。
- 次に、その項目内にあるチェックボタンをクリックします。
コードに示されているように、toMatchElement
関数を使用して itemName
が "Read Logto get-started document" に設定されている項目を一致させ、それから readDocItem
参照を使用してその下にある textContent
が 'Check' のボタンをクリックします。
readDocItem
を取得する際の CSS セレクターは li[class$=item]:has(div[class$=itemName])
であることに注意してください。このセレクターは、後で li
タグの下にある button
をクリックする予定のため、itemName
内部の li
要素のルート div
要素と一致させます。
expect(readDocItem).toClick
の使用方法は toMatchElement
と似ています。例のコードでは、ボタンの textContent
を一致させるために { text: 'Check' }
を追加します。ただし、ボタンのテキスト内容を一致させるかどうかは、必要に応じて決定できます。
外部リンクが新しいタブで開かれると期待する
次に、項目ノートの内部リンクをクリックすることで外部リンクが新しいタブで開かれるかどうかをテストしたいと考えています。
例のアプリでは、「Read Logto get-started document」項目には内のノートに Logto ドキュメントへの外部リンクがあることがわかりました。次に、テストコードを示します:
コードでは toClick
を使用して itemNotes
内のリンクをクリックします。
その後、browser.waitForTarget
を使用して "https://docs.logto.io/docs/tutorials/get-started" の URL を持つ新しく開かれたタブをキャプチャします。
タブを取得した後、target.page()
を使用して Logto ドキュメントページのインスタンスを取得し、さらにページが読み込まれているかどうかを確認します。
また、テストが完了した後、新しく開かれたページを閉じるのを忘れないでください。
フォームから項目が作成されることを期待する
さて、項目を追加する機能をテストしたいと考えています。
フォームに項目名と項目メモを入力し、「Add」ボタンをクリックし、追加したコンテンツがリストに表示されることを確認します:
注意するとおり、expect(page).toFill(inputSelector, content)
を使用してフォームに内容を入力します。この関数は、入力内容全体を content
に置き換えます。
既存の内容を置き換えずにテキストボックスに文字を追加したい場合は、page.type(selector, content)
を使用して、入力フィールドに希望の内容を直接追加できます。
しかし、フォームに入力するフィールドが多い場合、toFill
を複数回呼び出すのは不便です。この場合、次の方法で一括で入力することができます:
指定されたオブジェクトのキーは、入力フィールドの name 属性です。
まとめ
jest-puppeteer を使用してエンドツーエンドテストを行う際の一般的なテスト要件と対応するコード記述技法について、簡単な例を通じて紹介しました。この案内が、jest-puppeteer テストコードの基礎を早速学ぶ手助けとなることを願っています。