🤖

【playwright】PageObjectModelという実装パターン

2024/12/12に公開

Social Databank Advent Calendar 2024 の 11 日目です。

前回の記事
https://zenn.dev/sdb_blog/articles/test-blog-kyoya-004

🙃はじめに

皆さんおはこんばんにちは!(死語
充実したplaywright人生歩んでいますか!?
今回はplaywrightの実装パターンであるPageObjectModel についてお話ししていこうと思います!

🗒️Page Object Modelって?

皆さんはPageObjectModel(ページオブジェクトモデル:以下POM) を聞いたことはありますか?

ChatGPTに聞いてみた!

👶<playwrightにおけるPOMってなに!?

🤖<目的: 各ウェブページに対応するクラスを作成し、そのクラス内にページ要素や操作を定義します。
これにより、テストコードから直接ページの操作を記述するのではなく、抽象化されたメソッドを通じて操作を実行します。

とのこと。
また、メリットも以下のように提示してくれました💁‍♀️

メリット 説明
コードの再利用性 ページ操作をメソッド化することで、複数のテストで簡単に再利用できる。
保守性向上 ページ構造やセレクターが変更されても、POMクラス内の修正だけで対応可能。
可読性 テストコードが簡潔で直感的になり、操作フローや期待結果に集中できる。

簡単に言い換えると..?

ということになります!

ということで実際に実装してみましょう!!

  • テスト自動化用に公開されているテストサイトを使って実装してみます!
  • 実際のページに行って確認するとよりわかりやすいと思います!
以下のテストケースで実装
No. 操作手順
1 日本語ページの「トップページ」を選択
2 hotel予約ページに遷移
3 メニュータブから「宿泊予約」をクリック
4 「お得な特典付きプラン」のフィールドにある「このプランで予約」をクリック
5 新しいタブでページが開くのでそちらを選択する
6 それぞれ以下のように入力or選択する
・ 宿泊日:「○○/○○/○○(3ヶ月以内)」
・ 宿泊数:「1」
・ 人数:「2」
・ 追加プラン:「お得な観光プラン」
・ 氏名:「任意の名前」
・ 確認のご連絡:「希望しない」
・ ご要望:「もっと安くしてね〜」
7 「予約内容を確認する」をクリック
8 「この内容で予約する」をクリック
9 ポップアップが出るので「閉じる」を選択
10 期待値:ページが閉じてヘッダーに「HOTEL….」の要素があるページに戻ること

超絶愚直実装(ちょうぜつぐちょくじっそう)してみた

一度POMを利用していない愚直にテストコードを書いた例を下記に記載します!!

hotel_gutyoku.spec.ts
import {expect, test} from "@playwright/test";

test("hotelテストページで予約をする", async({ page })=>{
    // 最初のページに遷移
    await page.goto("https://hotel-example-site.takeyaqa.dev/");
    await page.getByText("トップページへ").click();
    // 2ページ目
    await page.getByRole("link", { name: "宿泊予約" }).click();
    await page.locator(".row").filter({ hasText: "お得な特典付きプラン" }).getByText("このプランで予約").click();
    // 3ページ目、新しいタブが開くので以下をそちらで操作
    const page1Promise = page.waitForEvent("popup");
    const newPage = await page1Promise;
    await newPage.getByLabel("宿泊日").fill("");
    await newPage.getByLabel("宿泊日").fill("2025/03/03");
    await newPage.getByLabel("宿泊数").fill("1");
    await newPage.getByLabel("人数").fill("2");
    await newPage.getByText("お得な観光プラン").check();
    await newPage.getByLabel("氏名").fill("プレイ 太郎");
    await newPage.getByLabel("確認のご連絡").selectOption("希望しない")
    await newPage.getByLabel("ご要望").fill("もっと安くしてね〜");
    await newPage.getByText("予約内容を確認する").click();
    // 4ページ目
    await newPage.getByText("この内容で予約する").click();
    await newPage.getByText("閉じる").click();
    // 最初のページに戻る
    await expect(page.locator("h2")).toContainText("宿泊プラン一覧");
});

(これでテストは通ります、是非コピペで試してください)


POM実装をしてみた

次にPOMを使って実装してみましょう!!
見た目すっきりしましたね(以下メソッド名適当につけちゃってますがご了承ください...🙇)

hotel_POM.spec.ts
test("hotelテストページで予約をする", async({ page })=>{
    // 1ページ目
    const hotelPage1 = new HotelPage1(page);
    await hotelPage1.goto();
    // 2ページ目
    const hotelPage2 = new HotelPage2(page);
    await hotelPage2.reserveSelectPlan();
    // 3ページ目、新しいタブが開くので以下をそちらで操作
    const page1Promise = page.waitForEvent("popup");
    const newPage = await page1Promise;
    const hotelPage3 = new HotelPage3(newPage);
    await hotelPage3.fillReservationDetails("2025/03/03","1","2","プレイ 太郎","希望しない","もっと安くしてね〜")
    // 4ページ目
    const hotelPage4 = new HotelPage4(newPage);
    await hotelPage4.clickReserve();
    // 最初のページに戻る
    await expect(page.locator("h2")).toContainText("宿泊プラン一覧");
});

なにをしてるか

以下が最初のページに対するページオブジェクトです

HotelPage1.ts
export default class TagPage extends VExplorerPage {
    readonly gotoPage;
    readonly topPageButton;

// 下記でボタンなどの要素を用意する
    constructor(page: Page) {
        super(page);
        this.topPageButton = page.getByText("トップページ");
    }
    // テストするページまでいき、「トップページ」ボタンをクリックしている
    async goto() {
        await this.page.goto("https://hotel-example-site.takeyaqa.dev/");
        await this.topPageButton.click();
    }
}

トップページのページオブジェクト

HotelPage2.ts
export default class TagPage extends VExplorerPage {
    readonly stayReserveLink;
    readonly selectPlanBtn;

    constructor(page: Page) {
        super(page);
        this.stayReserveLink = page.getByRole("link", { name: "宿泊予約" });
        this.selectPlanBtn = page.locator(".row").filter({ hasText: "お得な特典付きプラン" }).getByText("このプランで予約");
    }
// 宿泊予約リンクを踏み、プランを選択する
    async reserveSelectPlan() {
        await this.stayReserveLink.click();
        await this.selectPlanBtn.click();
    }
}


宿泊内容入力画面のページオブジェクト

HotelPage3.ts
export default class TagPage extends VExplorerPage {
    readonly stayDateInput;
    readonly stayCountInput;
    readonly guestCountInput;
    readonly addPlanNameInput;
    readonly reserverNameInput;
    readonly confirmContactSelector;
    readonly userRequestForm;
    readonly registerBtn;
    // それぞれの要素をここで用意してあげる
    constructor(page: Page) {
        super(page);
        this.stayDateInput = page.getByLabel("宿泊日");
        this.stayCountInput = page.getByLabel("宿泊数");
        this.guestCountInput = page.getByLabel("人数");
        this.addPlanNameInput = page.getByText("お得な観光プラン");
        this.addPlanNameInput = page.getByText("お得な観光プラン");
        this.reserverNameInput = page.getByLabel("氏名");
        this.confirmContactSelector = page.getByLabel("確認のご連絡");
        this.userRequestForm = page.getByLabel("ご要望");
        this.registerBtn = page.getByText("予約内容を確認する")
    }
    // hotel_POM.spec.tsから渡ってきた値がここで処理されていく
    async fillReservationDetails(stayDate: string, stayCount: string, guestCount: string,reserverName: string, chooseOption: string, userRequest: string) {
        await this.stayDateInput.fill("");
        await this.stayDateInput.fill(stayDate);
        await this.stayCountInput.fill(stayCount);
        await this.guestCountInput.fill(guestCount);
        await this.addPlanNameInput.check();
        await this.reserverNameInput.fill(reserverName);
        await this.confirmContactSelector.selectOption(chooseOption)
        await this.userRequestForm.fill(userRequest);
        await this.registerBtn.click();
    }
}

宿泊予約確認画面

HotelPage4.ts
export default class TagPage extends VExplorerPage {
    readonly registerBtn;
    readonly closedBtn;

    constructor(page: Page) {
        super(page);
        this.registerBtn = page.getByText("この内容で予約する")
        this.closedBtn = page.getByText("閉じる");
    }

    async clickReserve() {
        await this.registerBtn.click();
        await this.closedBtn.click();
    }
}

こんな感じで実装できちゃいます

🎄まとめ

いかがだったでしょうか。
私はPageObjectModelという実装パターンをとても気に入っています!🎭
実際にリリースでUI上などで修正が入った関係でテストが落ちるようになってしまった、なんてことがあったのですがPageObjectをちょこっと修正するだけで 「ことなきを得!!!!🥳」 となったのでとても恩恵を受けた思い出です。

是非皆さんもPOMをつかって良きplaywright生活を送りましょう!🪄

ソーシャルデータバンク テックブログ

Discussion