React・実務の道具立て

本物のブラウザで通しで試す(Playwright)

前回の単体テストでは届かない「ページ移動をまたぐ動き」をPlaywrightで確かめます。実際に3本書いて走らせ、strict modeで落ちたところまで載せました。何をE2Eにして何をしないかの線引きも扱います。

前回のテストは、部品ひとつを疑似的なDOM(jsdom)の上で動かすものでした。速くて、落ちた原因も分かりやすい。

ただ、こういうものは確かめられません。

  • カードを押したら詳細ページに移動する
  • ページを移動してもお気に入りが残っている
  • 実際のブラウザでそもそも表示されるか(CSSが崩れていないか)

これを見るのがE2Eテスト(End to End=端から端まで)です。本物のブラウザを起動して、人と同じように操作します

入れる

npm install --save-exact -D @playwright/test
npx playwright install chromium

2行目でブラウザ本体を落としてきます(数百MBあります)。chromiumを省くと3種類全部入ります。

設定を1つ置きます。

import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  use: { baseURL: 'http://localhost:5173' },
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: true,   // すでに起動していれば使い回す
  },
})

3本書いてみる

e2e/birds.spec.tsを作ります。

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

test('検索すると一覧がしぼりこまれる', async ({ page }) => {
  await page.goto('/')
  await expect(page.getByRole('heading', { name: 'しまちゃんのことりずかん' })).toBeVisible()

  await page.getByPlaceholder('名前やすんでいる場所でさがす').fill('フクロウ')

  await expect(page.getByText('1羽みつかりました')).toBeVisible()
  await expect(page.getByRole('link', { name: 'フクロウ' })).toBeVisible()
  await expect(page.getByRole('link', { name: 'スズメ' })).toBeHidden()
})

test('カードから詳細ページへ移動できる', async ({ page }) => {
  await page.goto('/')
  await page.getByRole('link', { name: 'ハクチョウ' }).click()

  await expect(page).toHaveURL(/\/birds\/8$/)
  await expect(page.getByRole('heading', { name: 'ハクチョウ' })).toBeVisible()
})

test('鳥を追加すると一覧に出る', async ({ page }) => {
  await page.goto('/')
  await page.getByLabel('名前').fill('ペンギン')
  await page.getByLabel('すんでいる場所').fill('なんきょく')
  await page.getByRole('button', { name: 'ずかんに追加' }).click()

  await expect(page.getByRole('link', { name: 'ペンギン' })).toBeVisible()
  await expect(page.getByText('9羽みつかりました')).toBeVisible()
})

探し方が前回と同じであることに気づいたでしょうか。getByRolegetByLabel——Testing Libraryで覚えた書き方が、そのまま使えます。

走らせる

npx playwright test
Running 3 tests using 1 worker

  ✓  1 e2e/birds.spec.ts:3:1 › 検索すると一覧がしぼりこまれる (272ms)
  ✓  2 e2e/birds.spec.ts:14:1 › カードから詳細ページへ移動できる (234ms)
  ✓  3 e2e/birds.spec.ts:22:1 › 鳥を追加すると一覧に出る (252ms)

  3 passed (1.3s)

ブラウザを起動して、検索して、ページを移動して、フォームを送って——それが1.3秒で終わります。手でやると1分はかかる確認です。

待ち方——sleepを書かない

E2Eが「不安定で当てにならない」と言われる原因は、ほぼこれです。

// ✗ 環境が遅いと落ちる。速い環境では無駄に待つ
await page.waitForTimeout(2000)
expect(await page.locator('.card').count()).toBe(9)

// ○ 条件が満たされるまで自動で待つ
await expect(page.getByText('9羽みつかりました')).toBeVisible()

expect(...)locatorを渡した場合、Playwrightは満たされるまで一定時間くり返し確認します。秒数を書く必要はありません。

便利な道具

コマンドやること
npx playwright codegen http://localhost:5173操作するとコードが書き出される。最初の1本はこれが速い
npx playwright test --ui画面つきで1ステップずつ実行・巻き戻し
npx playwright test --headedブラウザを表示して走らせる
npx playwright show-report失敗したところのスクリーンショットと操作の記録を見る

codegenは覚えておくと役に立ちます。手で操作するとそのままテストコードになるので、書き出してから不要な行を削るのが現実的な始め方です。

何をE2Eにするか

全部をE2Eで覆うのは失敗の定番です。遅く、壊れやすく、直すのが面倒になって、やがて誰も見なくなります。

確かめたいこと向いている場所
入力チェック・条件による出し分け単体テスト(前回)
関数の計算結果単体テスト
ページ移動をまたぐ流れE2E
ログイン→操作→ログアウトE2E
通信が失敗したときの表示E2E(通信を差しかえて試せます)
見た目が崩れていないかE2Eのスクリーンショット比較、または専用の道具

目安は「これが壊れたら、その日のうちに気づきたい」流れだけです。ことりずかんなら、検索・詳細への移動・追加の3本で足ります。

Reactテストを書く(Vitest + Testing Library)前回の単体テスト。getByRoleで探す理由は同じで、E2Eでもそのまま活きます

公式ドキュメントも見てみる

playwright.dev外部サイトWriting tests | PlaywrightPlaywright公式の「テストを書く」。locatorexpectの考え方、自動で待つ仕組みの説明がここにまとまっています(英語)

このレッスンのまとめ

  1. E2Eは本物のブラウザでアプリ全体を動かすテスト。ページ移動や通信をまたぐ流れを確かめられる
  2. playwright.config.tswebServerで開発サーバーを自動起動できる
  3. 探し方は単体テストと同じgetByRolegetByLabel
  4. 操作はすべて非同期。awaitを落とさない
  5. strict mode——指定が1つに決まらないと落ちる。.first()で逃げず、名前を正確に書く
  6. waitForTimeoutを書かないexpect(locator)は満たされるまで自動で待つ
  7. E2Eにするのは壊れたら困る導線だけ。細かい分岐は単体テストの担当
  8. CIではnpx playwright install --with-depsを忘れない

次は、部品を1つずつ並べて見られるようにするStorybookです。

やってみよう:導線を3本テストする

ことりずかんの主要な流れをE2Eにします。

  1. npm install --save-exact -D @playwright/testnpx playwright install chromium
  2. playwright.config.tsを作り、baseURLwebServerを書く
  3. e2e/birds.spec.ts検索・詳細への移動・追加の3本を書く
  4. npx playwright testで走らせる
  5. ★のクリックをgetByRole('button', { name: 'お気に入り' })と書いて、strict modeで落ちるところを見る
npx playwright test
npx playwright test --ui        # 1ステップずつ見たいとき

5番は飛ばさないでください。落ち方を1回見ておくと、本番で同じエラーが出たときに5秒で直せます

よくある質問

単体テストとE2Eテストはどう違いますか?
単体テストは部品ひとつを疑似的なDOMの上で動かします。E2Eテストは本物のブラウザでアプリ全体を動かすので、ページ移動・通信・URLの変化まで含めて確かめられます。落ちたときの原因は単体テストのほうが分かりやすく、現実に近いのはE2Eです。
E2Eテストは何本くらい書きますか?
壊れたら困る導線だけです。ログイン、検索、購入のような主要な流れを数本。全機能をE2Eで覆うと実行時間が伸び、ちょっとした変更で落ちるテストが増えて、やがて誰も見なくなります。
Playwrightとcypressのどちらを使いますか?
新規ならPlaywrightが選ばれることが多いです。複数ブラウザに標準で対応し、実行が速く、失敗したときに操作の記録(トレース)を残せます。既存の案件でCypressが動いていれば、そのまま使って構いません。
E2Eテストが不安定になるのですが。
待ち時間を秒数で指定しているのが原因のことが多いです。Playwrightのexpectは条件が満たされるまで自動で待つので、sleepを書かずにtoBeVisibleなどで待たせると安定します。