- PageObject パターン
従来のテストケースの課題
1)UIの変更に耐えられず、変更ごとに多数のテストケースを修正する必要がある
2)大量の共通コード(driver, find, click)が存在する
3)ビジネスロジックのテストケースを明確に表現できない
PageObject パターンの原則
メソッドの役割
- UIが提供する機能を共通メソッドとして表現する
- メソッドは他のPageObjectを返すか、アサーションに使用するデータを返す
- 同じ操作でも結果が異なる場合は異なるメソッドとしてモデル化する
- メソッド内でアサーションを行わない
フィールドの役割
- ページ内部の要素を外部に公開しない
- UIのすべての要素をモデル化する必要はない
POパターンによるカプセル化の主要構成要素
- Pageオブジェクト:ページの機能をカプセル化
- Driverオブジェクト:Web、Android、iOS、APIなどへのアクセスを担当
- テストケース:Pageオブジェクトを呼び出して業務ロジックを実行し、アサーションを行う
- データ管理:設定ファイルとデータ駆動型テストのためのデータ
- Utils:基本的なフレームワークの不足を補完する機能群
2. テストフレームワークの構築
POは設計思想であり、まずは全体像を描いてから詳細を埋めていく。企業微信アプリの実装を例に説明する。
1)pageパッケージを作成し、basepageモジュールを追加し、ベースクラスを作成する
basepage.py
from appium.webdriver.webdriver import WebDriver
class BasePage():
def __init__(self, driver: WebDriver = None):
self._driver = driver
2)appモジュールを作成し、AppクラスをBasePageから継承する
from testenter.page.basepage import BasePage
from testenter.page.main import Main
class App(BasePage):
def start(self):
return self
def stop(self):
pass
def restart(self):
pass
def main(self):
return Main(self._driver)
以下の点に注意:
startメソッドがreturn selfを返す理由:この戻り値がない場合、mainメソッドを呼び出すことができないstartメソッドをBasePageに移動してもよいが、今後のWeb対応を考慮して、別モジュールに分ける方が設計的に柔軟性があるdriverを渡しているが、BasePageから継承されるdriverはNoneなので重複は発生しない
3)Mainページに遷移する際、mainモジュールを作成する
from testenter.page.addresslistpage import AddresslistPage
from testenter.page.basepage import BasePage
class Main(BasePage):
def goto_message(self):
pass
def goto_addresslist(self):
return AddresslistPage(self._driver)
def goto_workbench(self):
pass
def goto_myprofile(self):
pass
ページ名が不明な場合は、adb shell dumpsys window | grep mCurrentで現在の画面名を取得できる。
4)addresslistpageモジュールを作成し、メンバー追加ボタンをクリックして「招待メンバー」ページへ遷移する。click_addmemberメソッドを定義する
from testenter.page.basepage import BasePage
class AddresslistPage(BasePage):
def click_addmember(self):
from testenter.page.memberinvitepage import MemberInvitePage
return MemberInvitePage(self._driver)
5)memberinvitepage.pyを作成し、「手動入力」ボタンをクリックして連絡先追加ページへ遷移するclick_memualaddメソッドを定義する
from testenter.page.basepage import BasePage
class MemberInvitePage(BasePage):
def click_memualadd(self):
from testenter.page.contactaddpage import ContactAddPage
return ContactAddPage(self._driver)
def click_back(self):
from testenter.page.addresslistpage import AddresslistPage
return AddresslistPage(self._driver)
6)contactaddpage.py(連絡先追加ページ)を作成し、名前、電話番号、性別を入力して保存し、「メンバー追加」ページに戻るclick_saveメソッドを定義する
class ContactAddPage(BasePage):
def input_name(self):
return self
def set_gender(self):
return self
def input_phone(self):
return self
def click_save(self):
from testenter.page.memberinvitepage import MemberInvitePage
return MemberInvitePage(self._driver)
7)ページ構造が整ったので、テストケースを作成する。test_caseパッケージを作成し、test_contact.pyに連絡先追加テストケースを記述する
from testenter.page.app import App
class TestAddContact():
def setup(self):
self.main = App().start().main()
def test_addtact(self):
self.main.goto_addresslist().click_addmember().click_memualadd(). \
input_name().input_phone(). \
set_gender().click_save().click_back()
⚠️⚠️❗️重要なポイント:なぜ一部のモジュールはメソッド内でのみインポートしているのか?最初はモジュールの先頭でインポートしていたがエラーが発生した。エラーの原因は循環インポートであった。
循環インポートとは、あるページが次のページをインポートし、その逆もまた同様に起こる状態である。Pythonはこのような循環参照に対応していない。
解決策としては、使用時にインポートを行う(ローカルインポート)ことで回避できる。これにより、初期化時の循環依存を避けることができる。