PR TIMESエディターのPlaywrightテストをPOM形式に移行して改善しました

  • URLをコピーしました!

こんにちは 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を試してみることを強くお勧めします!

  • URLをコピーしました!

この記事を書いた人

PR TIMES 開発本部インターン生

目次