開発チームがお届けするブログリレーです!既に公開されている記事もありますので、こちらから他のメンバーの投稿もぜひチェックしてみてください!

はじめに

私が担当している案件ではE2Eテストを導入して品質担保を行なっています。
web開発においてコード品質およびアプリの信頼性を担保する方法はいくつかありますが、今回はE2Eテストを導入するメリットと、実際にテストを組み込んでみる手順を簡単に紹介したいと思います。

なぜE2Eテストを導入するのか

  • webアプリの実機テストって大変
    • webアプリ開発の際、コードレビューや定期的なモンキーテストなどで実機操作を行なって動作チェックをすることは多いと思いますが、実案件ではOSごとに端末を用意する必要があったり、社用端末では管理コストがかかったり取り扱いに気を遣うことが多いかと思うので、開発スピードに少なからず影響が出てしまいがちです。
  • モンキーテストのような簡易的なテストだとデグレに気づかなかったりする
    • 開発メンバーが少なければ網羅できるケースも必然的に少なくなってしまいますし、きちんとしたテストを行おうとすると、都度テストケース作ったり、上述の通りテスト用の端末を手配する必要があったりします。
  • webアプリの統合テストを用意するのがコスト高め
    • 実際の運用に即した統合テストをE2Eテストを用いず作成しようとすると、APIやstoreのモック、ユーザー操作を再現するためのヘルパー関数の用意などなど、画面を跨ぐデータの受け渡しが大掛かりになりがちです。
    • そのため、品質の高いテストを書こうとすると、それに応じたスキルが要求されるのは悩みどころになります。
  • 単体テスト、統合テストを完璧に書いても、実機で問題なく動作するとは限らない
    • Reactを例にし、統合テストをJest + React Testing Libraryで書いたと仮定した場合、この時のDOMの挙動はjsdomに依存するため、各モダンブラウザの内部実装と異なる挙動をとる可能性があることは念頭におく必要があります。
    • 逆も然りで、ブラウザが独自実装を行っている場合もあるので、必ずしもW3C通りの動きになるわけでもありません。
    • そのため、統合テストをE2Eテストではない方法で網羅する場合でも、実装の詳細までテスト網羅しすぎないように、とは言われています。

導入

では、実際にE2Eテストを導入する方法を、順を追って解説していきたいと思います。
今回はツールにPlaywrightを用いることにします。
公式サイトはこちら
https://playwright.dev/

インストール

インストールコマンドが提供されているので、それを叩くだけで完了です。簡単。

npm init playwright@latest

テスト実行

インストールが完了した時点で、テストディレクトリが生成され、サンプルテストコードが作られていると思います。
その状態でテストを実行してみましょう。

npx playwright test

テスト結果はブラウザ上から確認することができます。
テストレポート表示は以下のコマンドで行うことができます。

npx playwright show-report

また、後述しますが、PlaywrightにはUIモードがあり、GUI上から任意のテストの実行から結果の表示まで行うこともできます。

npx playwright test --ui

Playwrightを支える強力なツール・機能

UI Mode

PlaywrightにはUIモードという強力なツールが備わっており、テスト作成に大きく役立ってくれます。
以下にUIモードでできることの一部を紹介させていただきます。

  • 各テストのスナップショットの確認
  • スナップショットをChrome DevToolsを使ってのデバッグ
  • コンソールに表示されるログ、通信リクエストのログの表示
  • テストがfailした原因の表示
  • テスト内で指定できるLocatorのサンプルの表示

UIモードでテストコマンドを実行すると以下のような画面が立ち上がると思います。
オレンジの枠がテストケースの一覧、緑枠がテスト実行時のタイムライン、赤枠がテストコード内のそれぞれのアクションのログ、青枠がAPIリクエストやJS側の標準出力が表示されるエリアをなっています。
これによってテストコードのデバッグがやりやすくなっているのはありがたいです。

Codegen

E2Eテストを作る際に結構面倒なのが、レンダリングされたHTMLのDOMをソースから調べて、そこに対してユーザーイベントを仕込むことかなと思います。
Playwrightの場合、Codedenの名のとおりテスト作成者が操作した内容をテストコード化できる機能が提供されています。

npx playwright codegen

上記コマンドを叩くと、ブラウザとコード生成ツールが立ち上がると思うので、あとは起動したブラウザ上で操作を行うだけで、テストがどんどん生成されていきます。とても便利。

 

Trace viewer

UIモードでテストを実行した時と同様、どのテストがどこでfailしたのかをログとして確認することができます。
Trace Viewerを利用したい場合は、--traceオプションを実行時に追加してあげればOKです。

npx playwright test --trace on

実行後、以下のようにアイコンが増えていると思います。そちらをクリックすることで内容を確認できるようになっています。
(Playwrightのバージョンによってはアイコンだけ表示されているかもしれません)

Playwright MCP

リリースされたばかり、かつ今回のスコープからは外れるので細かい解説を割愛しますが、AIコーディング支援ツール(GitHub Copilotなど)と連携させることによって、自然言語でE2Eテストを生成できるようになるようです。
こちらは気が向いたら記事にしてみようと思います。
リポジトリはこちら
https://github.com/microsoft/playwright-mcp

テストの書き方

一通りツールの紹介が終わったところで、実際にテストを書いていく手順を紹介してみたいと思います。
大まかですが、基本的には以下の手順で進めていくことになると思います。
1. await page.goto()メソッドで、テスト対象のURLに移動
2. await page.locator()メソッド、またはawait page.getBy***()メソッドでDOMを指定
3. 上記のDOMに対し、click()focus()などのメソッドで、実際のユーザーアクションをエミュレーション
4. expect()メソッドでDOMの状態に対しAssertionし、結果を比較する

こちらのTODOアプリを利用して、実際にテストを書くとこんな感じ。

import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc/#/');
await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('hogehoge'); // テキストボックスに対してテキストを入力し、
await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); // Enterキーを押下して追加する
await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('fugafuga');
await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter');
await page.getByRole('button', { name: 'Delete' }).click(); // 追加したtodoを削除
await page.getByRole('button', { name: 'Delete' }).click();
});

テストコードを書き慣れている人であれば、JestやVitestで書くような単体テストと手順はほぼ変わらないことがわかると思います。主な相違点は以下になります。

  • テスト対象がソースコードではなく、ローカル含め実際に稼働しているアプリそのもの
  • WAI-ARIA RoleだけでなくCSSセレクタやX PathでDOMの指定ができる
  • 概ね単体テストよりは実行に時間がかかる

CSSセレクタが使えるので、人によってはreact-testing-libraryを使った単体テストより書きやすい、と感じる人もいるかと思います。

単体テストとどう使い分けるか

  • 上述の通り実行時間は単体テストが勝るので、フォームの細かいバリデーションチェックや、ステートやストアに依存して変化するUIの完全な網羅はこちらに任せたほうが良いことが多いです。
    • 特にバリデーションはきちんとやっていればコンポーネントとロジックは分離しているはずなので、フォームの許容値を完全に網羅するのは、ロジック側に対してテストを流した方が良いかと思います。
  • 実環境で動かすことができるのが最大のメリットなので、ブラウザごとの仕様差分があるメソッド(もしくはあり得そうな実装)を使うときはE2Eテストで網羅すると良いかと思います。
    • 特に複数端末での同期チェックのような、動作環境を跨いだテストは単体テストでは書けず、手動テストも大変なのでE2Eテストでやる価値は高いです
  • APIを伴うテスト、または画面遷移を伴うテストをどちらで書くかは微妙なライン。
    • ケースバイケースなので心配であれば両方で書いておいた方が良いでしょう。
API込みのテストを単体テストで網羅する場合 API込みのE2Eテストで網羅する場合
メリット ・実行速度が速い
・TSでAPIの型定義を書いていればType Errorでミスに気づける
・ケース網羅が十分であれば、APIの仕様変更に強い
・テストケースごとにタイムアウトを設定できるので、ヘルスチェックや簡易的なパフォーマンスチェックの代わりになる
デメリット ・API側の変更を追従できていないとバグを見逃すことがある
・結合テストでは実APIを使って書くこともできるが、APIに認証が必要だと、それを突破させる手段が必要になる
・実環境のリソースにそれなりの負担がかかる
・ネットワークに依存するので安定しないこともあり

Tips

ここまでで概ねPlaywrightを使ったテストコードの書き方を紹介できたかと思います。
現在私の参画している案件でPlaywrightを使ったE2Eテストを導入して、およそ一年弱くらいになりますが、最後に実務でE2Eテストを取り入れ運用してみて、感じたこと、効果的にE2Eテストを運用するコツを紹介して記事を締めたいと思います。

  • テストケースは基本小分けにして、他のテストの成否に依存した作りにしない方が良い
    • 前提を伴うテストを増やしてしまうと並列実行しにくくなり、実行時間が増えてしまいがちです。また、前提テストがfailすると後続に影響するので、テストの安定性にも影響するので、可能な限り単独でテストが完結できるようにするのが良いかなと思います。
  • E2Eテストの更新タイミングは定期的に時間を設けるか、大きな開発がひと段落したときなど開発サイクルに影響しないタイミングの方が良い
    • リリース前でE2Eテストをガッツリ書いてしまうと、仕様が変わった際にテストも修正が必要になってしまうので、開発サイクルを犠牲にしがちになってしまうことが多いのかなと思います。
    • 同様の理由でPR提出時に単体テストを書かせるケースはあると思いますが、E2Eテストは必須にしない方が良いでしょう(もちろん、ケース分岐が少なければどんどん書いてしまったほうが品質に貢献します)
  • テストが安定しないときはタイムアウトを伸ばす方針ではなく、リトライ回数を増やす方向に倒した方が良い
    • テスト対象のブラウザやネットワークのパフォーマンスにもよりますが、DOM操作の過程でLocatorが参照できずにfailするケースが体感多く見受けられました。
    • ただし、一テストあたりの実行時間が長ければタイムアウトを伸ばす必要性もあります。実際にUIモードなどを活用し、テストの実行時間を見ながら良い塩梅の値を設定していくと良いでしょう。
    • タイムアウトの起因がAPIなどのネットワーク由来であれば、モック化してしまうのも一つの手段になります。実運用されているサーバーの値を使わなくなるため、E2Eテストの信頼性は少し落ちることになるので、ケースバイケースで使うと良いかと思います(大量のデータをテストで用いたいとき、ユーザーごとに個別の設定値をAPIで持たせているときなど)

以上です。