対象
- E2Eテストが初めての方
- PlaywrightでE2Eテストを実装する方法が知りたい方
E2Eテストとは?
E2E(End-to-End)テストは、アプリケーションの全体的な動作をシミュレートし、ユーザーの視点からアプリケーションをテストする方法です。E2Eテストは、ユーザーがアプリケーションを使用する際の実際の操作を模倣し、システムの機能やユーザーエクスペリエンスに関する問題を検出するのに役立ちます。
PCが自動でブラウザを立ち上げ、コードで指示した動作をポチポチと動かしてくれます。
開発現場では、「最低限の動作保証をする自動テスト」、という認識です。
リリース前に実行することでデグレを防ぐのが主な役割になります。
メリット
- 最低限の品質保証になる。
- 面倒な手動テストの工数削減になる。
デメリット
- 実装コストが高い
- メンテが大変
- 動作が不安定で、時々失敗するテストが出てくる。
実際にサーバーにアクセスして実行するので、どうしても不安定になりがちです。
実装やメンテも大変なので、必要最低限でテストを書くのが良いでしょう。
もっと細かい観点のテストはvitestなどで書くことをおすすめします。
サンプルアプリ
簡単なToDoアプリをE2Eテストで動作保証します。(サインインが必要です)
一覧
追加
「New ToDo」を押すとモーダルが出る。
編集
編集アイコンを押すとモーダルが出る。
完了ボタン
チェックを押すと完了になる。
削除ボタン
ゴミ箱アイコンを押すと削除。
準備
npm init playwright@latest
こちらでセットアップを行います。
セットアップ完了後、テスト対象のURLを定数として定義しておきます。
export const TODO_APP_URL = process.env.TODO_APP_URL || 'http://localhost:3000';
必要であれば.envを入れておきましょう。
npm install dotenv --save
playwright.config.tsにconfigDotenv()を追記すると読み取り可能となります。
import { defineConfig, devices } from '@playwright/test';
import { configDotenv } from 'dotenv';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
configDotenv();
テストするブラウザをchromeに固定する設定を行います。
playwright.config.tsを編集します。
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
firefoxとwebkitをコメントアウトしてください。
vscode拡張機能をインストールしておきましょう。
タイトル表示のテスト
tests/todo.spec.tsを作成します。
import { test, expect } from '@playwright/test';
import { TODO_APP_URL } from '../../constants';
test('タイトルの表示', async ({ page }) => {
await page.goto(TODO_APP_URL);
await expect(page).toHaveTitle(/My Sample ToDo List/);
});
test項目の左側に矢印のアイコンが表示されているので、押下するとテストが実行されます。
ブラウザが開いてテストが実行されるはずです。
実行中のコードの場所が一目でわかるようになっています。
vscode左側の砂時計マークを選択すると、実行したテストの内容を確認できます。
追加機能のテスト
ToDo追加するための手順はこちら👇
- 「New ToDo」ボタンを押す
- モーダルが表示される
- 内容を入力する
- 「作成する」ボタンを押す
- 追加されたことを確認
この手順通りにブラウザを動かしてみます。
add.spec.tsを作成しましょう。
test('ToDoを追加', async ({ page }) => {
await page.goto(TODO_APP_URL);
// 「New ToDo」ボタンを押す
const newTodoButton = page.getByRole('button', { name: 'New ToDo' });
await newTodoButton.click();
// モーダルが表示される
// 内容を入力する
// 「作成する」ボタンを押す
// 追加されたことを確認
});
getByRoleでボタンを取得し、クリックするだけの内容です。
getByRoleは、WAI-ARIA ロールで要素を取得できるようになっています。
ボタンだとrole=”button”、dialogだとrole=”dialog”のように決まっています。
これで実行すると、
モーダルが表示されました!
ここまでうまく動作していそうです。
では、続きを実装していきます。
「モーダルが表示される」ことを検証します。
await page.goto(TODO_APP_URL);
// 「New ToDo」ボタンを押す
const newTodoButton = page.getByRole('button', { name: 'New ToDo' });
await newTodoButton.click();
// モーダルが表示される
const modal = page.getByRole('dialog');
expect(modal).toBeVisible();
toBeVisible()で検証しています。
試しにボタンをクリックする箇所をコメントアウトして実行してみましょう。
const newTodoButton = page.getByRole('button', { name: 'New ToDo' });
// await newTodoButton.click();
ちゃんと落ちてくれました。
このようにあえて失敗して通らないことを確かめるのも、テストを書くうえで大切です。
続いて、ToDoの内容を入力します。
// 内容を入力する
const input = page.getByRole("textbox");
await input.fill("免許更新");
fill()メソッドでinput要素に内容を埋めることができます。
入力したら作成します。
// 「作成する」ボタンを押す
const createButton = page.getByRole('button', { name: '作成する' });
await createButton.click();
追加されていれば動作OKです。
最後に追加されていることを検証します。
first()メソッドで一番上のli要素を取得し、そのテキストが「免許更新」
であることを確認します。
// 追加されたことを確認
const newToDo = page.getByRole("listitem").first();
await expect(newToDo).toHaveText("免許更新");
これでパスすれば、追加機能のテストはOKです!
「免許更新」を他のテキストに変えると落ちることも確認してみてください。
今回は「免許更新」で入力しましたが、何度も実行すると同じ内容のToDoが並ぶので追加できなくてもテストが通ってしまいます。
なるべく入力値は一意にしたいので、timestampに変更してみましょう。
// 内容を入力する
const input = page.getByRole("textbox");
const timestamp = new Date().getTime().toString();
await input.fill(timestamp);
// 「作成する」ボタンを押す
const createButton = page.getByRole('button', { name: '作成する' });
await createButton.click();
// 追加されたことを確認
const newToDo = page.getByRole("listitem").first();
await expect(newToDo).toHaveText(timestamp);
これでパスすればOKです!
編集と削除の機能もトライしてみてください!
編集機能のテスト
test('ToDoを編集', async ({ page }) => {
await page.goto(TODO_APP_URL);
// 先頭のToDoを取得
const firstToDo = page.getByRole("listitem").first();
const editButton = firstToDo.getByLabel("edit");
await editButton.click();
// モーダルが表示される
const modal = page.getByRole('dialog');
expect(modal).toBeVisible();
// 内容を編集する
const input = page.getByRole("textbox");
const timestamp = new Date().getTime().toString();
await input.fill(timestamp);
// 「更新する」ボタンを押す
const updateButton = page.getByRole('button', { name: '更新する' });
await updateButton.click();
// 編集されたことを確認
await expect(firstToDo).toHaveText(timestamp);
});
削除機能のテスト
test("ToDoを削除", async ({ page }) => {
await page.goto(TODO_APP_URL);
await page.waitForTimeout(1000);
const toDoCountBefore = await page.getByRole("listitem").count();
const firstToDo = page.getByRole("listitem").first();
const deleteButton = firstToDo.getByLabel("Delete");
await deleteButton.click();
const toDoCountAfter = await page.getByRole("listitem").count();
// 個数が1つ減っていることを確認
expect(toDoCountBefore).toBe(toDoCountAfter + 1);
});
CIで定期実行する
今回はGitHub Actionsで5分おきに定期実行してみます。
(実際はstg環境にデプロイされるたび、毎日深夜3時に、など要件によって様々です。)
Playwrightをインストール時にGitHub Actionsを入れるか?のように聞かれるので、Yesにしておくとテンプレートが出来上がります。今回はこれを利用します。
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
デフォルトだとpush時とプルリク時に実行されるので、これを5分おきに変更します。
name: Playwright Tests
on:
schedule:
- cron: '*/5 * * * *'
jobs:
次に環境変数を設定します。
settingsタブの、 secrets and variablesから設定可能です。
TODO_APP_URLを設定します。
- name: Set Up .env file
run: echo "TODO_APP_URL=${{ secrets.TODO_APP_URL }}" >> .env
- name: Install dependencies
run: npm ci
これで定期実行されるようになりました。
あとはSlackと連携して、成功or失敗の通知を送るのも良いですね!
E2Eテストをうまく使ってバグを防いでいきましょう!
ここまでのコードはこちらです👇