Python
Selenium
ブラウザオートメーション
ブラウザ自動化
PageObjectDesignPattern


概要

この記事ではPOM(Page Object Model)を使ったブラウザ自動化ツールの開発方法を説明します。

入門編なので内容は軽めです。


環境


  • 言語: Python3

  • ブラウザ: Chrome


やること


  1. ブラウザ自動化ツール」というキーワードでググる

  2. 「約 〇〇〇〇 件 (〇〇 秒) 」という検索結果ページ上部に表示されるテキストを取得してprint()関数で出力する


準備


  • Seleniumのインストール(入っていない場合)

    pip install selenium


  • Chromeドライバのダウンロード

    以下のリンクからご利用のChromeのバージョンに適したドライバをダウンロードし、作業ディレクトリに設置します。

    http://chromedriver.chromium.org/downloads



POM(Page Object Model)とは?

POM(Page Object Model)は、ページをクラスオブジェクトとして扱うブラウザ自動化ツールのデザインパターンの一つです。

あまり耳馴染みがないかもしれませんが、海外のSelenium界隈の記事でよくこの手法が紹介されています。

他にも「Page Object Design Pattern」や「Page Object Pattern」と呼ばれていて、日本語で書かれた記事などはこちらの方がヒットするかと思います。


POMの概念

POMには主に以下のような概念があります。


  • ページオブジェクトクラス(Page Object Class)

  • ロケータ(Locators)

ロケータは必ずしも取り入れる必要はないですが、有ると非常に便利です。


POMを取り入れるメリット

POMを利用すると以下の様なメリットがあります。


  • 複数のテストケースで再利用可能なコードが作成可能

  • 重複コードを減らせる

  • ユーザーインターフェースに変更が合った際に変更が容易


まずは普通に実装してみる

まずはPOMを使わずに実装してみます。今回は説明を簡素化するためHTML要素は全てXPathで取得します。

ChromeでXPathを取る・検証する


app.py

# -*- coding: utf-8 -*-

from time import sleep

from selenium import webdriver
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome('./chromedriver')
driver.get('https://www.google.com/')

search_box = driver.find_element_by_xpath('//*[@id="tsf"]/div[2]/div/div[1]/div/div[1]/input')
search_box.send_keys('ブラウザ自動化ツール' + Keys.ENTER)

result_stats = driver.find_element_by_xpath('//*[@id="resultStats"]').text
print(result_stats)

sleep(1)
driver.quit()



実行

自動的にChromeが立ち上がり、「ブラウザ自動化ツール」というキーワードで検索した後に検索結果の件数のテキストが出力され、ブラウザが自動的に閉じます。


出力結果

約 941,000 件 (0.32 秒)



コードを分解して解説

必要なモジュールをインポートしています。Keysを使うとエンターキーを押したりする処理を簡単に実装できます。

インポートする順番はPythonのコーディング規約のPEP8に準拠しています。


from time import sleep

from selenium import webdriver
from selenium.webdriver.common.keys import Keys

Chromeドライバをセットしてグーグルを開いています。

driver = webdriver.Chrome('./chromedriver')

driver.get('https://www.google.com/')

グーグルの検索窓のHTML要素をXPathで取得し、「ブラウザ自動化ツール」とキーワードを入力してからEnterキーを押しています。

search_box = driver.find_element_by_xpath('//*[@id="tsf"]/div[2]/div/div[1]/div/div[1]/input')

search_box.send_keys('ブラウザ自動化ツール' + Keys.ENTER)

XPathで取得した検索結果の件数のテキストをresult_stats変数に代入し、print()関数で出力してます。

result_stats = driver.find_element_by_xpath('//*[@id="resultStats"]').text

print(result_stats)

sleep()関数で1秒処理を止めた後、driver.quit()でブラウザを閉じています。sleep処理は無くてもいいのですが、一瞬でブラウザが閉じてしまうのは嫌なのでここでは使用しています。

sleep(1)

driver.quit()


POM(Page Objects Model)で書き直す

さて、ここからが本題で、先ほどのコードをPOM(Page Object Model)で書き直してみます。


ページオブジェクトクラス(Page Object Class)

今回はGoogleの検索と検索結果の2つのページが存在するので、SearchPageクラスとResultPageクラスを用意して、基本的な処理をまとめたBasePageクラスを継承させます。


pages.py

# -*- coding: utf-8 -*-

from selenium.webdriver.common.keys import Keys

class BasePage:

def __init__(self, driver=None, url=None):
self.driver = driver
self.url = url

def open(self):
self.driver.get(self.url)

def close(self):
self.driver.quit()

class SearchPage(BasePage):

def __init__(self, driver):
url = 'https://www.google.com/'
super().__init__(driver=driver, url=url)

def search(self, keyword):
search_box = self.driver.find_element_by_xpath('//*[@id="tsf"]/div[2]/div/div[1]/div/div[1]/input')
search_box.send_keys(keyword + Keys.ENTER)

class ResultPage(BasePage):

def __init__(self, driver):
super().__init__(driver=driver)

def get_result_stats(self):
result_stats = self.driver.find_element_by_xpath('//*[@id="resultStats"]').text
return result_stats


ページクラスを使用するようにapp.pyを書き直します。

大分スッキリして可読性が上がった気がします。


app.py

# -*- coding: utf-8 -*-

from time import sleep

from selenium import webdriver

from pages import SearchPage
from pages import ResultPage

driver = webdriver.Chrome('./chromedriver')

search_page = SearchPage(driver)
search_page.open()
search_page.search('ブラウザ自動化ツール')

result_page = ResultPage(search_page.driver)
print(result_page.get_result_stats())

sleep(1)
result_page.close()



ロケータ(Locators)

ページクラスだけでも十分そうですが、POMではHTML要素の取得をロケータ(Locators)として外出しにする事が推奨されているので、各ページのロケータを作ってLocators.pyにまとめます。


Locators.py

# -*- coding: utf-8 -*-

from selenium.webdriver.common.by import By

class SearchPageLocator:

def __init__(self):
pass

search_box = (By.XPATH, '//*[@id="tsf"]/div[2]/div/div[1]/div/div[1]/input')

class ResultPageLocator:

def __init__(self):
pass

result_stats = (By.XPATH, '//*[@id="resultStats"]')


ロケータを用意したので、pages.pyを以下のように修正します。


pages.py

# -*- coding: utf-8 -*-

from selenium.webdriver.common.keys import Keys

from Locators import SearchPageLocator
from Locators import ResultPageLocator

class BasePage:

def __init__(self, driver=None, url=None):
self.driver = driver
self.url = url

def open(self):
self.driver.get(self.url)

def close(self):
self.driver.quit()

class SearchPage(BasePage):

def __init__(self, driver):
url = 'https://www.google.com/'
super().__init__(driver=driver, url=url)

def search(self, keyword):
locator = SearchPageLocator.search_box
search_box = self.driver.find_element(*locator)
search_box.send_keys(keyword + Keys.ENTER)

class ResultPage(BasePage):

def __init__(self, driver):
super().__init__(driver=driver)

def get_result_stats(self):
locator = ResultPageLocator.result_stats
result_stats = self.driver.find_element(*locator).text
return result_stats



もう一度実行してみる


出力結果

約 941,000 件 (0.25 秒)


動きは変わらないですね。

これでPOMでの書き直しは完了です。


idやclassで要素を取得

自分で作ったシステムの動作テストを行う場合は、XPathではなくidやclass、nameで要素を取得したいかと思います。

今回のコードだと、Locators.pyの要素取得している箇所を以下のように書き換える事で可能です。

# idで要素を取得

elem_by_id = (By.CSS_SELECTOR, '#ID名')

# classで要素を取得
elem_by_class = (By.CSS_SELECTOR, '.クラス名')

# nameで要素を取得
elem_by_name = (By.Name, 'ネーム')

ポイントはidとclassはBy.IDBy.Classのようにならないところです。

リスト型でロケータを定義しているのはfind_elementメソッドの引数に展開して渡すためです。

ちなみにByの中身ですが以下のように定数を持っているだけのクラスオブジェクトになっています。

class By(object):

"""
Set of supported locator strategies.
"""

ID = "id"
XPATH = "xpath"
LINK_TEXT = "link text"
PARTIAL_LINK_TEXT = "partial link text"
NAME = "name"
TAG_NAME = "tag name"
CLASS_NAME = "class name"
CSS_SELECTOR = "css selector"


要素オブジェクトクラス(Element Object Class)

POMではコードの重複をさらに減らす為、HTML要素をクラスオブジェクトにする事もできます。

せっかくなのでinput要素をクラスオブジェクトにしてみました。

find()メソッドでは、WebDriverWaitexpected_conditionsを使って要素が確認されてから取得処理を行うように改良しています。


elements.py

# -*- coding: utf-8 -*-

from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class BaseElement:

def __init__(self, driver, locator):
self.driver = driver
self.locator = locator
self.element = self.find()

def find(self):
return WebDriverWait(
self.driver, 10).until(
EC.visibility_of_element_located(self.locator))

class InputElement(BaseElement):

def search(self, keyword):
self.element.send_keys(keyword + Keys.ENTER)


InputElementクラスを使ってSearchPageクラスを以下のように書き換えられます。


pages.py


from elements import InputElement

class SearchPage(BasePage):

def __init__(self, driver):
url = 'https://www.google.com/'
super().__init__(driver=driver, url=url)

@property
def search_box(self):
locator = SearchPageLocator.search_box
return InputElement(
driver=self.driver,
locator=locator
)


app.pyの検索部分は以下のようになります。


app.py

search_page = SearchPage(driver)

search_page.open()
search_page.search_box.search('ブラウザ自動化ツール')

ただ、ここまでやるとかえってコードが複雑化する気もするので、HTML要素をクラスオブジェクトとして扱うかどうかは好みが分かれそうなところです。


まとめ

Seleniumについての記事は沢山ありましたが、POMに関して触れている記事はあまり見かけなかったので紹介してみました。

Pythonのunittestを使ってブラウザテストツールを作る際にも非常に役に立ちますので、興味ある方はぜひPOM試してみてください。


参照

Page Objects(非公式ドキュメント)

Page Object Model (POM) & Page Factory: Selenium WebDriver Tutorial

Udemyコース

Beginning Selenium WebDriver Testing in Python

Elegant Browser Automation with Python and Selenium