Pythonで、Seleniumのfind_element系メソッドとPageObjectsを試してみた

PythonSeleniumには

  • find_element_by_xxx()
  • find_elements_by_xxx()
  • find_element()
  • find_elements()

などのメソッドが用意されています。

今回は、それらを試してみた時のメモを残します。

なお、途中からSeleniumのPage Objectsパターンが出てきます。

ただ、今回使うPage Objectsは公式ドキュメントよりは簡易的なPage Objectsになっています。
6. Page Objects — Selenium Python Bindings 2 documentation

 
目次

 

環境

  • Python 3.6.3
  • Selenium 3.6.0
  • pytest 3.2.3
    • テストランナーとして使用

 

Seleniumの対象とするHTML

以下のHTMLを用意しました。

なお、id属性が重複していますが、これは意図的にやっています。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <ol>
        <li id="id1">Text1</li>
        <li id="id1">Text2</li>
        <li id="id1">Text3</li>
    </ol>

    <ul>
        <li class="post1">Post1-1</li>
        <li class="post1">Post1-2</li>
        <li class="post2">Post2</li>
    </ul>
</body>
</html>

 
あとは、上記のindex.htmlファイルがあるディレクトリにて、

$ python -m http.server

を実行し、 http://localhost:8000 でHTMLファイルが表示されるようにします。
21.22. http.server — HTTP サーバ — Python 3.6.3 ドキュメント

 

find_element_by_xxx()

HTMLから

などで要素があるかを検索し、WebElement(selenium.webdriver.remote.webelement.WebElement)を返します。
WebElement - 7. WebDriver API — Selenium Python Bindings 2 documentation

 
また、複数の要素がHTMLにあった場合は、そのうち最初に見つかったものを返します。

class TestFindElement:
    def setup_method(self):
        self.chrome = Chrome()
        self.chrome.get('http://localhost:8000')

    def teardown_method(self):
        self.chrome.quit()

    def test_find_element_by_xxx(self):
        id1 = self.chrome.find_element_by_id('id1')
        assert id1.text == 'Text1'

        post1_1 = self.chrome.find_element_by_class_name('post1')
        assert post1_1.text == 'Post1-1'

        post1_2 = self.chrome.find_element_by_css_selector('.post1')
        assert post1_2.text == 'Post1-1'

        post1_3 = self.chrome.find_element_by_xpath('//li[@class="post1"]')
        assert post1_3.text == 'Post1-1'

 
なお、要素が見つからない場合は、例外 selenium.common.exceptions.NoSuchElementException を送出します。

def test_find_elemnt_by_xxx_without_element(self):
    try:
        self.chrome.find_element_by_id('id2')
    except NoSuchElementException:
        # エラー内容をprintする
        print(f'\n{traceback.format_exc()}')

 

find_elements_by_xxx()

find_element_by_xxx()と同じく、HTMLから

などで要素があるかを検索します。

戻り値は、WebElementのリストとなります。

def test_find_elements_by_xxx(self):
    id1 = self.chrome.find_elements_by_id('id1')
    assert len(id1) == 3
    assert id1[0].text == 'Text1'

    post1_1 = self.chrome.find_elements_by_class_name('post1')
    assert len(post1_1) == 2
    assert post1_1[0].text == 'Post1-1'

    post1_2 = self.chrome.find_elements_by_css_selector('.post1')
    assert len(post1_2) == 2
    assert post1_2[0].text == 'Post1-1'

    post1_3 = self.chrome.find_elements_by_xpath('//li[@class="post1"]')
    assert len(post1_3) == 2
    assert post1_3[0].text == 'Post1-1'

 
なお、find_element_by_xxx()と異なり、要素が見つからない場合は、長さ0のリストを返します。

そのため、要素の有無をテストする場合、例外の送出がないこちらを使うのが良いかもしれません。

def test_find_elements_by_xxx_without_element(self):
    post3 = self.chrome.find_elements_by_class_name('post3')
    assert len(post3) == 0

 

find_element()

最初、find_element_by_xxx()があるならいらないのでは?と思っていました。

しかし、SeleniumでPageObjectsパターンを使う場合は、これを使うのが良さそうだと分かりました。公式ドキュメントにも記載されています。
6. Page Objects — Selenium Python Bindings 2 documentation

 
今回は、簡易的なPageObjectsを実装してみます。

まず、index.html向けのクラス IndexPageObject を用意します。そのクラスには

  • ページを操作するため、WebDriverのインスタンスを渡す
  • Seleniumで検索する要素を、クラス定数のタプルとして定義

を実装します。

class IndexPageObject:
    # Seleniumで探す要素をクラス定数として定義する
    ID1 = (By.ID, 'id1')
    CLASS1 = (By.CLASS_NAME, 'post1')
    CLASS2 = (By.CLASS_NAME, 'post2')
    CLASS3 = (By.CLASS_NAME, 'post3')  # これは存在しない要素
    CSS = (By.CSS_SELECTOR, '.post1')
    XPATH = (By.XPATH, '//li[@class="post1"]')

    def __init__(self, driver: WebDriver):
        # WebDriverをインスタンスに渡す
        self.driver = driver

 
次に、ページに対する操作をメソッドとして実装します。

ページの要素を探す場合は、find_element()メソッドを使います*1
find_element() - 7. WebDriver API — Selenium Python Bindings 2 documentation

また、find_element()メソッドの引数には、クラス定数として定義したタプルを展開して渡します。

これにより、もしHTML構造が変わっても、クラス定数のタプルの内容を変更するだけです。テストコードには影響しません。

def get_post_using_find_element(self):
    # IDで探す場合
    id1 = self.driver.find_element(*IndexPageObject.ID1)

 
あとは、テストコードの方で

します。

def test_find_element(self):
    index_page = IndexPageObject(self.chrome)
    post = index_page.get_post_using_find_element()
    assert post == 'Post1-1'

   
もし、要素が存在しない場合は、find_element_by_xxx()と同じく例外を送出します。

def get_post_using_find_element_without_element(self):
    try:
        # find_element()の場合、要素が存在しないと例外が送出される
        return self.driver.find_element(*IndexPageObject.CLASS3)
    except NoSuchElementException:
        print(f'\n{traceback.format_exc()}')
        raise

 
テストコードはこんな感じです。

def test_find_element_without_element(self):
    with pytest.raises(NoSuchElementException):
        index_page = IndexPageObject(self.chrome)
        index_page.get_post_using_find_element_without_element()

 

find_elements()

こちらは、find_elements_by_xxx()をPageObjectsで実装する場合に使います。

def get_post_count_using_find_elements(self):
    # XPATHで探す場合
    xpath1 = self.driver.find_elements(*IndexPageObject.XPATH)
    return len(xpath1)

 
テストコードです。

def test_find_elements(self):
    index_page = IndexPageObject(self.chrome)
    count = index_page.get_post_count_using_find_elements()
    assert count == 2

 
要素が存在しない場合は、find_elements_by_xxx()と同様長さ0のリストが返ってきます。

def get_post_count_using_find_elements_without_element(self):
    # find_elements()の場合は、要素が存在しなくても例外とならず、長さ0のリストが返る
    elements = self.driver.find_elements(*IndexPageObject.CLASS3)
    return len(elements)

 
テストコードです。

def test_find_elements_without_element(self):
    index_page = IndexPageObject(self.chrome)
    count = index_page.get_post_count_using_find_elements_without_element()
    assert count == 0

 

テストの実行

いずれのテストもパスしました。

$ python -m pytest . -v -s
==== test session starts ====
platform darwin -- Python 3.6.3, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 -- /path/to/bin/python
cachedir: .cache
rootdir: /path/to/python_selenium_sample/e.g._find_element, inifile:
collected 8 items                                                                                                                                                         

test_selenium_find_element.py::TestFindElement::test_find_element_by_xxx PASSED
test_selenium_find_element.py::TestFindElement::test_find_elemnt_by_xxx_without_element 
Traceback (most recent call last):
  File "/path/to/python_selenium_sample/e.g._find_element/test_selenium_find_element.py", line 33, in test_find_elemnt_by_xxx_without_element
    self.chrome.find_element_by_id('id2')
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 341, in find_element_by_id
    return self.find_element(by=By.ID, value=id_)
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 843, in find_element
    'value': value})['value']
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 308, in execute
    self.error_handler.check_response(response)
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/errorhandler.py", line 194, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"id","selector":"id2"}
  (Session info: chrome=62.0.3202.75)
  (Driver info: chromedriver=2.33.506106 (8a06c39c4582fbfbab6966dbb1c38a9173bfb1a2),platform=Mac OS X 10.11.6 x86_64)


PASSED
test_selenium_find_element.py::TestFindElement::test_find_elements_by_xxx PASSED
test_selenium_find_element.py::TestFindElement::test_find_elements_by_xxx_without_element PASSED
test_selenium_find_element.py::TestFindElement::test_find_element PASSED
test_selenium_find_element.py::TestFindElement::test_find_element_without_element 
Traceback (most recent call last):
  File "/path/to/python_selenium_sample/e.g._find_element/index_page_object.py", line 41, in get_post_using_find_element_without_element
    return self.driver.find_element(*IndexPageObject.CLASS3)
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 843, in find_element
    'value': value})['value']
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 308, in execute
    self.error_handler.check_response(response)
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/errorhandler.py", line 194, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"class name","selector":"post3"}
  (Session info: chrome=62.0.3202.75)
  (Driver info: chromedriver=2.33.506106 (8a06c39c4582fbfbab6966dbb1c38a9173bfb1a2),platform=Mac OS X 10.11.6 x86_64)


PASSED
test_selenium_find_element.py::TestFindElement::test_find_elements PASSED
test_selenium_find_element.py::TestFindElement::test_find_elements_without_element PASSED

==== 8 passed in 20.19 seconds ====

 

ソースコード

GitHubに上げました。e.g._find_elementディレクトリ以下が、今回のファイルです。
thinkAmi-sandbox/python_selenium-sample

 
なお、説明用にPageObjectsでassertしていますが、本来は不要です。

*1:公式ドキュメントには、「‘Private’ method used by the find_element_by* methods. Use the corresponding find_element_by* instead of this.」と書いてあるので、使ってよいか不安ですが...