こんにちは PR TIMES開発本部のインターンの Chanoknan です。
PR TIMESエディターのフロントエンドテスト戦略開発の一環として、エディターのPlaywright統合テストをPage Object Model(POM)パターンを使ってリファクタリングしました。このブログでは、このリファクタリングについて話したいと思います。
PR TIMESエディターのフロントエンドテスト戦略については、以下の記事で詳しく読むことができます。
問題点
最初は、テストを直接テストファイルに記述していました。各テストにはAPIモックとUI操作が必要で、多くの冗長性が生じていました。エディターの統合テストが増えるにつれて、異なるテスト間で同じAPIモックとUI操作を繰り返していることに気づきました。これにより、何か変更があった場合、すべてのテストをリファクタリングする必要があったため、テストの保守が難しくなりました。不要なコードを削減し、保守性を向上させるために、POMデザインパターンを使用することにしました。
Page Object Model(POM)とは?
Page Object Model(POM)は、テストロジックとUI操作を分離することを促進するデザインパターンです。簡単に言えば、POMはウェブページやコンポーネントを表すPage Objectsを作成し、そのページとのすべての操作をカプセル化することを可能にします。これにより、テストコードをよりモジュール化し、保守が容易で、より読みやすくすることができます。
POMがテストを改善する方法:
- カプセル化:UI操作はテストから抽象化され、テストロジックがよりクリーンになります。
- 再利用性:ページオブジェクトは異なるテスト間で再利用でき、冗長性を減らします。
- 保守性:UIの変更(例:セレクタの変更)があった場合、それぞれの個別のテストではなく、Page Objectだけを変更すればよいです。
API モックのリファクタリング
POMによる最大の改善点の一つはAPIモックの処理でした。最初は、すべてのテストがbeforeEachブロックにAPIモックを含める必要がありました。
POM使用前:beforeEachでのAPIモック
test.describe('プレスリリース編集画面でSPとPCの表示切替ができること', () => {
let page: Page;
test.beforeEach(async ({ browser, baseURL }) => {
page = await setUsePressReleaseEditorV3dot2(browser, baseURL!);
await fakeNow(page);
await mockNewRelic(page);
// ===== API の mock =====
await page.route(
'/api/press_release_editor.php/press_release/1',
async (route) => route.fulfill({ json: getPressReleaseResponseBody }),
);
...
// ===== 校正API の mock =====
await page.route('/api/proofreading.php/main', async (route) =>
route.fulfill({ json: postProofreadingMainResponseBody }),
);
...
await page.route('/api/proofreading.php/lint/*', async (route) =>
route.fulfill({ json: getProofreadingLintResultsEmptyResponseBody }),
);
});
});すべてのテストがこれらのAPIモックを必要としていたため、それらをPOMクラスにリファクタリングしました。
POMメソット:専用クラスでのAPIモック
/**
* エディターに最低限必要な API のモック
*/
async setEditorApiMock(
pressReleaseResponseBody = getPressReleaseResponseBody
) {
await fakeNow(this.page);
await mockNewRelic(this.page);
await this.page.route(
'/api/press_release_editor.php/press_release/1',
async (route) =>
route.fulfill({
json: pressReleaseResponseBody,
}),
);
}
.../**
* 校正 API の Mock
*/
async setProofreadingApiMock({
mainResponseBody,
lintResponseBody,
}: {
mainResponseBody?: object;
lintResponseBody?: object;
}) {
await this.page.route('/api/proofreading.php/main', async (route) =>
route.fulfill({
json: mainResponseBody ?? postProofreadingMainResponseBody,
}),
);
await this.page.route('/api/proofreading.php/lint', async (route) =>
route.fulfill({ json: postProofreadingLintResponseBody }),
);
await this.page.route('/api/proofreading.php/lint/*', async (route) =>
route.fulfill({
json: lintResponseBody ?? getProofreadingLintResultsEmptyResponseBody,
}),
);
}これで、テスト内でこれらのメソッドを次のように再利用できます:
// ===== API の mock =====
await editor.setEditorApiMock(getPressReleaseResponseBody);
// 校正 API mock
await editor.proofreading.setProofreadingApiMock({
mainResponseBody: postProofreadingMainResponseBody,
lintResponseBody: getProofreadingLintResultsEmptyResponseBody,
});POMを使った動的APIレスポンスの処理
この設定により、テスト間でAPIモックを共有できます。さらに、異なるテストケースが異なるAPIレスポンスを必要とする場合、必要な動作を変数として渡すことができます。
例:テストでのカスタムAPIレスポンス
test('予約配信を下書き保存でキャンセルできること', async ({
browser,
baseURL,
}) => {
const page = await setUsePressReleaseEditorV3dot2(browser, baseURL!);
editor = new SetEditorPage(page);
// 予約配信を下書き保存でキャンセルする場合のレスポンス
await editor.setEditorApiMock(reservedPressReleaseResponse);異なる校正APIレスポンスの処理:
// 校正が見つかった場合
await editor.proofreading.setProofreadingApiMock({
mainResponseBody: postProofreadingMainSubTitleResponseBody,
lintResponseBody: postProofreadingLintResponseBody,
});
// 校正が見つからない場合
await editor.proofreading.setProofreadingApiMock({
mainResponseBody: postProofreadingMainResponseBody,
lintResponseBody: getProofreadingLintResultsEmptyResponseBody,
});この柔軟性により、テストがより動的で保守しやすくなります。
POMを使ったUI操作のリファクタリング
プレスリリース配信 追加情報設定に関連するテストでは、頻繁に必要な情報を入力する必要がありました。すべてのテストに同じコードを書く代わりに、このロジックをPOMクラスに移動し、再利用可能にしました。
例えば、プレスリリース配信 追加情報設定に関連するテストでは、頻繁に必要な情報を入力する必要がありました。すべてのテストに同じコードを書く代わりに、このロジックをPOMクラスに移動し、再利用可能にしました。

各テストには繰り返しのUI操作が含まれており、保守が難しくなっていました。
POM使用後:カプセル化されたUI操作
/**
* 目的の設定
*/
async setPurpose() {
await this.page.getByRole('combobox', { name: '目的を選択' }).click();
await this.page
.getByRole('button', {
name: 'マーケティング/PR に関連する項目を表示する',
})
.click();
await this.page
.getByRole('group', { name: 'マーケティング/PR' })
.getByRole('option', { name: '新サービス開始' })
.click();
}
/**
* 種類の設定
*/
async setKind(kindText = '商品サービス') {
await this.page.locator('button', { hasText: '種類を選択する' }).click();
await this.page.getByRole('option', { name: kindText }).click();
}
/**
* ビジネスカテゴリの設定
*/
async setBusinessCategory() {
...
}
これで、テスト内で単純に以下のように呼び出すことができます:
// 目的・種類・ビジネスカテゴリを設定
await editor.step1.setPurpose();
await editor.step1.setKind();
await editor.step1.setBusinessCategory();これにより、テストファイルをクリーンで保守しやすく保つことができます。
結論
Playwright統合テストに Page Object Model(POM)パターンを採用することで、次のことを達成しました:
- 重複が少ない、よりクリーンなテストコード
- APIルートやUI要素が変更された場合の、より容易な保守
- APIモックとUI操作のための、より良い再利用性
このアプローチにより、PR TIMESエディターが進化し続けるにつれて、テストがより拡張性が高く、信頼性の高いものになります。
Playwrightを使用してテストの保守性を向上させたい場合は、POMを試してみることを強くお勧めします!


