前回のテストは、部品ひとつを疑似的なDOM(jsdom)の上で動かすものでした。速くて、落ちた原因も分かりやすい。
ただ、こういうものは確かめられません。
- カードを押したら詳細ページに移動するか
- ページを移動してもお気に入りが残っているか
- 実際のブラウザでそもそも表示されるか(CSSが崩れていないか)
これを見るのがE2Eテスト(End to End=端から端まで)です。本物のブラウザを起動して、人と同じように操作します。
入れる
npm install --save-exact -D @playwright/test
npx playwright install chromium2行目でブラウザ本体を落としてきます(数百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()
})探し方が前回と同じであることに気づいたでしょうか。getByRole・getByLabel——Testing Libraryで覚えた書き方が、そのまま使えます。
走らせる
npx playwright testRunning 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公式の「テストを書く」。locatorとexpectの考え方、自動で待つ仕組みの説明がここにまとまっています(英語)このレッスンのまとめ
- E2Eは本物のブラウザでアプリ全体を動かすテスト。ページ移動や通信をまたぐ流れを確かめられる
playwright.config.tsのwebServerで開発サーバーを自動起動できる- 探し方は単体テストと同じ
getByRole・getByLabel - 操作はすべて非同期。
awaitを落とさない - strict mode——指定が1つに決まらないと落ちる。
.first()で逃げず、名前を正確に書く waitForTimeoutを書かない。expect(locator)は満たされるまで自動で待つ- E2Eにするのは壊れたら困る導線だけ。細かい分岐は単体テストの担当
- CIでは
npx playwright install --with-depsを忘れない
次は、部品を1つずつ並べて見られるようにするStorybookです。