この記事は、ラクスパートナーズ AdventCalendar 2025の25日目の記事です。
(個人で25日連続投稿にチャレンジ中のカレンダーになります)
いよいよ最終日を迎えました!
以前、Playwrightの特徴や使い方を以下の記事でご紹介させていただきました。
Playwright公式がテストを書く際の哲学(テストを実装する上での考え方)とベストプラクティスについて記載していたので、今回はこちらの内容をまとめたいと思います。
哲学
ユーザーの目に見える部分をテストする
エンドユーザーはページ上にレンダリングされた内容を表示したり操作するため、E2Eテストでも同じようにレンダリングされた要素だけを表示したり操作するべき、という考え方です。
普段ユーザーが認識できない実装の裏側部分(関数の名前、配列であるかどうか、CSSのクラスは何かなど)にテストが依存しないようにする必要があります。
実装の裏側部分については、単体テストや結合テストの出番だと思うので、合わせて実装できると良さそうですね。
テストを分離する
各テストは他のテストから完全に分離され、独自のLocal Storage、Session Storage、データ、Cookieなどを使用して実行される必要があります。分離しておく理由は、テストがまとめて失敗することを防いだり、デバッグしやすくするためです。
beforeEachやafterEach という機能を使えば、各テストの実行前、実行後に同じ処理を実行してくれるので、コードの再利用に繋がります。
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
console.log(`Running ${test.info().title}`);
await page.goto('https://my.start.url/');
});
test('my test', async ({ page }) => {
expect(page.url()).toBe('https://my.start.url/');
});
ちなみに、テストファイルのスコープ内でbeforeEachやafterEachを呼び出した場合は、そのファイル内の各テストの前に実行されます。
test.describe()内で呼び出された場合は、各テストの前に実行されます。
import { test, expect } from '@playwright/test';
test.describe('two tests', () => {
// 同じdescribe内のtest関数でのみ実行
test.beforeEach(async ({ page }) => {
console.log(`Running ${test.info().title}`);
await page.goto('https://my.start.url/');
});
test('one', async ({ page }) => {
// ...
});
test('two', async ({ page }) => {
// ...
});
});
サードパーティのテストを避ける
自分が管理していない外部サイトやサードパーティのサーバーへのリンクをテストしない、というものです。
- 時間がかかり、テストの速度が低下する
- リンク先のページのコンテンツを制御できない可能性がある
といった理由のためです。
私も「指定した外部URLに遷移できるか」をテストしたことがあるのですが、タイムアウトでテストがよく失敗していたので避けた方が良さそうです。
どうしても外部サイトにアクセスしなければならないときは、Playwright Network APIでモックを作成して検証する、という方法が推奨されていました。
データベースを使うテストではデータを自分でコントロールできるようにする
データを固定化しないと、テストの結果が毎回変わってしまうためです。
ステージング環境でテストして、変更がないことを確認する
本番環境に近いステージング環境でテストして、結果に変更がないことを確認する、というものです。
ビジュアルリグレッションテストではOSやブラウザのバージョンを揃える
OSやブラウザのバージョンが違うと、レイアウトが微妙に変わる可能性があるためです。
そのため、定期的にPlaywrightも最新版に更新し、最新のOSやブラウザを取得した状態でテストする必要があります。
npm install -D @playwright/test@latest
ここまでがテストを実装する上での哲学の部分になります。
次はE2Eテストを実装する上でのベストプラクティスです。
ベストプラクティス
Locatorを使う
E2Eテストを書くには、まずHTML要素を見つける必要があります。これはPlaywrightに組み込まれているLocatorを使って実現できます。Locatorには自動待機機能と再思考機能が備わっています。
- 自動定期機能とは?
- Playwrightが要素に対して「この要素は操作することが可能か」を様々な観点でチェックしてくれる機能のこと
- 例:ボタンをクリックする前に、そのボタンが表示され有効になっていることを確認する
- 再思考機能とは?
- アサーションの
expect()が、指定した条件を満たすかをタイムアウトになるまで繰り返しチェックすること
- アサーションの
この2つの機能があるおかげでテストを安定的に実行できるのもPlaywrightの特徴の一つとなります。
公式が推奨しているLocatorは以下です。
- page.getByRole():WAI-ARIAのロール属性の値を指定して要素を取得できます
- page.getByText():指定したテキストの内容で要素を取得できます
- page.getByLabel():指定した値を持つlabelタグに囲まれたフォームの要素(inputタグなど)を取得できます
- page.getByPlaceholder():指定したプレースホルダーを持つinputタグを取得できます
- page.getByAltText():alt属性に指定した値を持つimgタグを取得できます
- page.getByTitle():title属性に指定した値を持つ要素を取得できます
- page.getByTestId():data-testid属性に指定した値を持つ要素を取得できます
参考:https://playwright.dev/docs/locators
Locatorの連鎖
取得したい要素を絞り込めるよう、Locatorを連鎖させる使い方も推奨されていました。
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
また、DOMは簡単に変わるので、XPathやCSSセレクタを使って要素を取得する(DOMの構造に依存する)とテストが失敗しやすくなります。
それよりも、先ほどご紹介したLocatorを使って要素を取得する方がDOMの変更に強いとされています。
page.locator('button.buttonIcon.episode-actions-later');
page.getByRole('button', { name: 'submit' });
Test generatorを使ってLocatorを生成する
npx playwright codegenをターミナルで実行して生成すると手軽です。
(VS Codeの拡張機能を使ってLocatorを生成することもできるようです)
VS Codeの拡張機能でデバッグする
VS CodeでPlaywrightの拡張機能を入れてデバッグする方法も紹介されていました。
全てのブラウザでテストする
playwright.config.jsで、どのブラウザでテストするかを指定できます(特定の端末を指定してのエミュレート機能もここで設定できます)。
また、テストのタイムアウト時間も設定することができます。
// @ts-check
import { defineConfig, devices } from '@playwright/test';
/**
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
/* PC版のブラウザでのテスト */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* モバイル端末でのエミュレート機能 */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
// テストのタイムアウトの延長
timeout: 10_000, // testのtimeoutを延長(10000ms)
expect: {
timeout: 10_000 // expectのtimeoutを延長(10000ms)
}
});
CI/CDでテストを実行する
主にGitHub ActionsでテストのCI/CDを実装する方法が紹介されていました。
(Github以外にも、GitLabなど他の環境での実装方法も紹介されていました)
テストにLintを適用する
TypeScriptと@typescript-eslint/no-floating-promisesを使用して、awaitの抜けがないかを確認することが推奨されていました。
並列処理とシャーディング
Playwrightは、デフォルトでテストを並列実行します。ただし、同じ単一ファイル内のテストは、同じワーカープロセスで順番に実行します。
そのため、同じファイル内に多くのテストがある場合は、以下のように
test.describe.configure( mode: 'parallel' )
を設定することで、同じファイル内でもテストを並列に実行することができます。
import { test } from '@playwright/test';
// 同じファイル内のテストを並列に実行する(スコープ全体に適用される)
test.describe.configure({ mode: 'parallel' });
test('runs in parallel 1', async ({ page }) => { /* ... */ });
test('runs in parallel 2', async ({ page }) => { /* ... */ });
あとは、テストスイートを分割して、複数のマシンで実行することでテストを高速化する、という方法もあります(これをシャーディングと呼びます)
CIで複数のマシンを同時に使って、テストを高速化したいときに使われるようです。
例えば、テストを3台のマシンに分割したい場合は、各マシンに以下のコマンドを実行させます。
マシンA:npx playwright test --shard=1/3
マシンB:npx playwright test --shard=2/3
マシンC:npx playwright test --shard=3/3
以上となります。
PlaywrightはTest Agentsという「AIを使ってテストを自動的に実装してくれる機能」が実装されたこともあり、より簡単にE2Eテストを実装できるようになりました(Playwright Test Agentsについては以下の記事で紹介しています)。
「E2Eテストは管理が大変そうなんだよな…」
と抵抗を感じている方は、ぜひPlaywrightを検討してみてください。
高速で安定したテストを手軽に実装することができるので、非常に心強い存在になると思います。
ここまで読んでいただき、ありがとうございました。