Playwright fixture 活用術:テスト間での画面状態の再利用パターン
情報システム室の進地@日比谷です。
Playwrightを活用すれば、E2Eテストを柔軟に組み上げることができます。しかし、E2Eテストが増えてくるとそのうち、下記のような状況に遭遇することがままあるかと思います。
- 同じような画面操作の繰り返しが増えてくる
- テストコード量が増えてきてメンテナンスが大変になってくる
- テスト実行時間が増えてきてとても時間がかかる、タイムアウトが続発する
特に最後が一番クリティカルだと思います。テスト時間が長くなって失敗しやすくなると単純にテストの実行回数が減ってしまいます。テストにとって一番良くないことは、テストが実施されないことなので、これは工夫して避けたいところです。
そこで、QAエンジニアの長友さんに相談し、教えていただいたFixtureを活用することでテスト間で画面状態を再利用して、こうした問題を解決できましたのでご紹介します。
Fixtureを使って特定操作を行った状態のPageオブジェクトを用意する
Fixtureを使うことで画面に対する特定操作をまとめて、その時点のPageオブジェクトを保持させることができます。
// tests/fixtures/search.ts
import { test as base } from '@playwright/test';
import { Page } from '@playwright/test';
// カスタムフィクスチャの型定義
type CustomFixture = {
searchedPage: Page;
};
// フィクスチャの拡張
export const test = base.extend<CustomFixture>({
// 検索結果の状態を得る
searchedPage: async ({ page }, use) => {
// テスト対象のページへアクセスする
await page.goto('https://example.com/example.html');
// 例)検索実行する
await page.fill('#keyword', '検索キーワード');
await page.getByRole('button', { name: '検索する' }).click();
await use(page);
}
});
export { expect } from '@playwright/test';
このFixtureを使って、検索結果のページに対して検証を行うことができます。
import { test, expect } from './fixtures/search';
// testを並列実行する
test.describe.configure({ mode: 'parallel' });
test.describe('検索結果に対するテスト群', () => {
test('タイトルに「成功」が含まれている', async ({ searchedPage }) => {
expect.soft(await searchedPage.locator('.title'), `「成功」が見つかりません`).toHaveText(/成功/);
});
test('検索結果が10件以上表示されている', async ({ searchedPage }) => {
const trs = await searchedPage.locator('table tbody tr').all();
expect.soft(trs.length, '検索結果が10件未満です').toBeGreaterThanOrEqual(10);
});
});
テストはtest.describe.configure({ mode: 'parallel' });
の記述により並列実行されます。そのため、Fixtureで初回実行によって生成されたsearchedPage
はテスト間(testメソッド間)で再利用されます。
さらに追加の特定操作を加えた後のPageオブジェクトを用意する
さらに、前述の検索結果に対して何か特定操作を加えた後のPageオブジェクトも用意できます。
// tests/fixtures/search.ts
import { test as base } from '@playwright/test';
import { Page } from '@playwright/test';
// カスタムフィクスチャの型定義
type CustomFixture = {
searchedPage: Page;
executedRemoveItemPage: Page;
};
// フィクスチャの拡張
export const test = base.extend<CustomFixture>({
// 検索結果の状態を得る
searchedPage: async ({ page }, use) => {
// テスト対象のページへアクセスする
await page.goto('https://example.com/example.html');
// 例)検索実行する
await page.fill('#keyword', '検索キーワード');
await page.getByRole('button', { name: '検索する' }).click();
await use(page);
},
executedRemoveItemPage: async ({ searchedPage }, use) => {
// 例)1行目の要素を削除
await searchedPage.locator('table tbody tr').nth(1).getByRole('button', { name: '削除する' }).click();
await use(searchedPage);
},
});
export { expect } from '@playwright/test';
使い方は最初の例と同じです。
import { test, expect } from './fixtures/search';
// testを並列実行する
test.describe.configure({ mode: 'parallel' });
test.describe('検索結果に対するテスト群', () => {
test('タイトルに「成功」が含まれている', async ({ searchedPage }) => {
expect.soft(await searchedPage.locator('.title'), `「成功」が見つかりません`).toHaveText(/成功/);
});
test('検索結果が10件表示されている', async ({ searchedPage }) => {
const trs = await searchedPage.locator('table tbody tr').all();
expect.soft(trs.length, '検索結果が10件ではありません').toBe(10);
});
test('削除実行した後、タイトルに「成功」が含まれている', async ({ executedRemoveItemPage }) => {
expect.soft(await executedRemoveItemPage.locator('.title'), `「成功」が見つかりません`).toHaveText(/成功/);
});
test('削除実行した後、表示件数が1件減っている', async ({ executedRemoveItemPage }) => {
const trs = await executedRemoveItemPage.locator('table tbody tr').all();
expect.soft(trs.length, '検索結果が10-1県ではありません').toBe(10-1);
});
});
まとめ
Fixtureを使うことでE2Eテストにおける同一、類似操作の集約やキャッシュ、ページオブジェクトの共有、再利用などを進めることができます。テストコードの保守性を向上させ、テスト実行速度の向上にも繋がりますのでぜひ活用してみてください。