ゼロから始めるPOM入門 [Python3 + Selenium + POM]

# 概要
この記事では**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を取る・検証する](https://qiita.com/ywindish/items/5a992c49387d81df900e)

```python: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**](https://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html#module-selenium.webdriver.common.keys)を使うとエンターキーを押したりする処理を簡単に実装できます。

インポートする順番はPythonのコーディング規約の[**PEP8**](https://www.python.org/dev/peps/pep-0008/#imports)に準拠しています。

```python

from time import sleep

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

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


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

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

```python
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()```関数で出力してます。

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

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

```python
sleep(1)
driver.quit()
```

# POM(Page Objects Model)で書き直す

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

### ページオブジェクトクラス（Page Object Class）

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


```python: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```を書き直します。
大分スッキリして可読性が上がった気がします。

```python: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```にまとめます。

```python: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```を以下のように修正します。

```python: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``の要素取得している箇所を以下のように書き換える事で可能です。

```python
# idで要素を取得
elem_by_id = (By.CSS_SELECTOR, '#ID名')

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

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

```

ポイントはidとclassは``By.ID``や``By.Class``のようにならないところです。
リスト型でロケータを定義しているのは[**find_element**](https://seleniumhq.github.io/selenium/docs/api/py/webdriver_remote/selenium.webdriver.remote.webdriver.html?highlight=find_element#selenium.webdriver.remote.webdriver.WebDriver.find_element)メソッドの引数に展開して渡すためです。

ちなみに[**By**](https://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.by.html?highlight=by#selenium.webdriver.common.by)の中身ですが以下のように定数を持っているだけのクラスオブジェクトになっています。



```Python
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()``メソッドでは、[**WebDriverWait**](https://seleniumhq.github.io/selenium/docs/api/py/webdriver_support/selenium.webdriver.support.wait.html?highlight=webdriver%20wait#module-selenium.webdriver.support.wait)と[**expected_conditions**](https://seleniumhq.github.io/selenium/docs/api/py/webdriver_support/selenium.webdriver.support.expected_conditions.html?highlight=expected_conditions#selenium.webdriver.support.expected_conditions)を使って要素が確認されてから取得処理を行うように改良しています。

```python: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``クラスを以下のように書き換えられます。

```python: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``の検索部分は以下のようになります。

```python:app.py
search_page = SearchPage(driver)
search_page.open()
search_page.search_box.search('ブラウザ自動化ツール')
```

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

# まとめ

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

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

# 参照
[Page Objects（非公式ドキュメント）](https://selenium-python.readthedocs.io/page-objects.html)
[Page Object Model (POM) & Page Factory: Selenium WebDriver Tutorial](https://www.guru99.com/page-object-model-pom-page-factory-in-selenium-ultimate-guide.html)



Udemyコース
[Beginning Selenium WebDriver Testing in Python](https://www.udemy.com/beginning-selenium-webdriver-testing-in-python/)
[Elegant Browser Automation with Python and Selenium](https://www.udemy.com/elegant-browser-automation-with-python-and-selenium/)