RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

【E2Eテスト】ページオブジェクトモデルを使ったらメンテ地獄から解放された話

こんにちは!フロントエンド開発課のkoki_matsuraです。
この記事では、僕が開発に携わっている製品のE2Eテストに取り入れたページオブジェクトモデル(POM)という実装パターンの概要と取り入れたキッカケ、POMへリファクタリングする簡単な例をご紹介させていただきます。
僕と同じようにE2Eテストに関わっている方、E2Eテストに興味を持っている方などに読んでいただけると幸いです。 目次は下記のようになっています。

POMとは

Webアプリケーションのテスト自動化において、テストコードとWebページを分離して管理する手法です。 POMを使わない従来のテストコードはWebページと分離しないため、どうしてもDOMの構造を意識したものになってしまい、コードは長くなり読みにくくなります。大規模になると保守性なども問題になってきます。
下図はPOMを使わないテストです。同じ要素を複数回取得する必要があり、テストコードの重複もあります。

POMはこれらの問題点を解消します。具体的にはWebページごとのクラス(ページオブジェクト)を作成し、各クラス内でそのページの要素に対する操作をメソッドとして定義します。テストコード側はページオブジェクトを呼び出し、メソッドを使うだけで要素を操作できます。
下図はPOMを使ったテストです。テストコードとWebページは分離されています。テストコードはDOMを意識せずにメソッドとアサーションだけを使えば簡単にテストが書けます。 まとめるとPOMの利点は以下が考えられます。

  • 可読性の向上
    テストコードではDOMを意識しなくて済むため、簡潔なコードとなり読みやすくなると考えられます。
  • 保守性の向上
    DOMに変更があっても、ページオブジェクトを変更するだけでテストコードの変更は不要になるため、保守性は向上します。
  • 再利用の向上
    ページオブジェクトを作成しておけば、そのページの様々なテストケースにおいてコードの重複を防ぐことができます。
  • チーム開発の効率化
    テストコードをチームメンバーで共有しやすくなります。また、テストコードを迅速に作成することができます。

なぜPOMを使い始めたのか

上記で利点をいくつか挙げましたが、その中でもPOMを使い始めた一番の理由は「保守性の向上」です。
POMを使う前は愚直に多くのページに対して テストコードを書いていました。
テストコードを書き続けるうちに1つ1つのページに対してのテストケースが多くなっていき、1つのケースに対して関係してくるページも多くなっていました。
そして、ある日、DOMを変更した瞬間、今まで問題なく通っていたE2Eテストは落ちてしまいました。1箇所の変更だけでもそのDOMが関わるテストは下図のように全て落ちてしまいます。

そこからは何か仕様変更が起こるたびに下記のようなループが起きます。
「仕様変更発生」→「DOMの変更」→「大量のテストが落ちる」→「大量のテストの修正」→「仕様変更発生」→ ...以下略
DOMの変更点が多い日には超大量のテスト修正という虚無の時間が訪れます。
このループが続いているとテストの失敗を放置してしまうようになってしまいます。
「せっかくE2Eテストを書いたのに...」「でも、毎回メンテナンスするなんて...」「もっと修正点が少なくなればいいのに...」とメンタル的にもしんどくなってきました。
同じような経験をした人や今現在している人もいるのではないでしょうか。
テストの失敗が続いてしまうと、信頼性も下がってきます。テストの意味もなくなってきます。もっと保守性の高いコードにするべきです。
そこでPlaywrightのドキュメントを読んでいたときに見つけたのが「ページオブジェクトモデル」です。

  • 今までのWebページとテストコードの間にページオブジェクトを挟むことでページをオブジェクトとして扱える!
  • DOMの変更が起きても、修正するのは該当のページオブジェクトでテストコードは修正しなくて済む!
  • 操作をメソッド化すればテストコードが簡潔になり、テストコードに慣れていない人でも簡単に読める!
  • ページオブジェクトを作る手間はかかるけど、あの虚無の日々を考えたら全然大したことない!
  • 保守性の高いテストが書ける!

と思ったため、導入するに至りました。
今では仕様変更が発生しても、下図のようにページオブジェクトのみを修正すればすぐに全てのテストが動くようになり、大幅に修正の時間も減り、保守性を高めることができました。

POMへのリファクタリング

POMの概要とどのような経緯で使うことに至ったのかの説明をしてきました。
最後は実際に簡単なログイン画面をもとに普通のテストコードからどのようにPOMへリファクタリングしていくかを説明させていただきます。

ログイン画面

ログイン画面は以下のようにシンプルに「名前」と「パスワード」の入力欄があります。

テスト内容

  1. ログイン画面へ遷移する
  2. 名前入力欄に「user」を入力
  3. パスワード入力欄に「password」を入力
  4. ログインボタンをクリック
  5. 一覧画面へ遷移できているかテスト

POM導入前のテストコード

名前入力欄、パスワード入力欄の要素へ記入し、ログインボタン要素をクリックしています。 ログイン後、トップ画面へ遷移できているかはURLを見ることでチェックします。
※ 下記のコードではパスワードをベタ書きしていますが、Gitなどに上げる場合にはenvファイルを経由するなどして直接は書き込まないようにしてください。

// login.spec.ts
import { test, expect } from '@playwright/test';

test("ログインできているか", async ({page}) => {
  await page.goto("/login");
  await page.getByLabel('名前').fill("user")
  await page.getByLabel('パスワード').fill("password")
  await page.getByRole('button', { name: 'ログイン' }).click();
  await expect(page).toHaveURL("/")
})

ページオブジェクト作成

ページオブジェクトを作成するために適当なディレクトリでpageObjectフォルダを作成します。
おすすめとしてはプロジェクト直下にE2Eフォルダを作成し、その中でE2E/tests/*.spec.tsE2E/pageObject/*.tsなどを管理するのが良いと思います。
では、ページオブジェクトを作成していきます。
基本は下記のような「要素の定義」「コンストラクタの定義」「操作の関数定義」の形で作成します。
goto関数やlogin関数はこのテストでは必須です。
waitForPageContentsのような読み込みを待つ関数は要素の読み込みを待たないことによるテストの失敗が多い場合に定義すると良いと思います。

// loginPage.ts
import { expect, Locator, Page } from '@playwright/test';

export class LoginPage {
    // 要素の定義
    readonly page : Page
    readonly name : Locator
    readonly password : Locator
    readonly loginButton : Locator
    
    // コンストラクタの定義
    constructor(page : Page) {
        this.page = page
        this.name = page.getByLabel("名前")
        this.password = page.getByLabel("パスワード")
        this.loginButton = page.getByRole('button', { name: 'ログイン' })
    }
 
    // 関数の定義
    async goto() {
        await this.page.goto("/login")
    }

    async waitForPageContents() {
        await this.name.waitFor()
        await this.password.waitFor()
        await this.loginButton.waitFor()
    }

    async login(name:string, password:string) {
        await this.name.fill(name)
        await this.password.fill(password)
        await this.loginButton.click()
    }
}

POM導入後のテストコード

ログインページのページオブジェクトが完成したので、実際にテストコードをリファクタリングしていきましょう。
やることは簡単で、テストコードの先頭でインスタンスを生成して、あとは操作を記述していくだけです。
今回の場合は下記のようなコードになります。

import { test, expect } from '@playwright/test';
import { LoginPage } from '../pageObject/loginPage';

test("ログインできているか", async ({page}) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login("user", "password");
    await expect(page).toHaveURL("/");
})

ページオブジェクトを用いてログインのテストをリファクタリングができました。
テストコードはDOMを意識したものではなくなっているので仕様変更が起きてもテストコードを変更する必要がありません。

終わりに

今回はE2Eテストにページオブジェクトモデルを導入した話をさせていただきました。
どうでしょうか。ページオブジェクトモデルの導入によりテストコードは多少見やすくなりましたが、色々実装することを考えると微妙だなと感じた人もいるのではないでしょうか。
僕も最初はそのように感じました。保守性を高めるメリットに対して、ページオブジェクト作成とテスト改修のコストがかかるというデメリットがあるため、微妙だと感じやすいです。 しかし、テストケースが増えたり、テストする画面が増えたりなど、大規模になればなる程、ページオブジェクトモデルは真価を発揮するものです。
なので、自分が開発している製品の規模感に合わせて導入するかを検討するのが良いと思います。
ここまで読んでいただきありがとうございます。この記事を機に、少しでもページオブジェクトモデルに興味を持っていただけたら幸いです。

Copyright © RAKUS Co., Ltd. All rights reserved.