Разделы портала

Онлайн-тренинги

.
Легкое веб-тестирование с Python, Pytest и Selenium WebDriver, часть 5: создание теста Page Object Selenium при помощи Python
05.08.2020 00:00

Автор: Энди Найт (Andy Knight)
Оригинал статьи
Перевод: Ольга Алифанова

Тест-функция, которую мы написали в прошлый раз, была неплоха, но ее можно улучшить, переработав при помощи паттерна Page Object.

Проблемы тестирования с WebDriver

Для справки, вот нынешняя версия нашей тест-функции:

  1. def test_basic_duckduckgo_search(browser):
  2. URL = 'https://www.duckduckgo.com'
  3. PHRASE = 'panda'
  4. browser.get(URL)
  5. search_input = browser.find_element_by_id('search_form_input_homepage')
  6. search_input.send_keys(PHRASE + Keys.RETURN)
  7. link_divs = browser.find_elements_by_css_selector('#links > div')
  8. assert len(link_divs) > 0
  9. xpath = f"//div[@id='links']//*[contains(text(), '{PHRASE}')]"
  10. phrase_results = browser.find_elements_by_xpath(xpath)
  11. assert len(phrase_results) > 0
  12. search_input = browser.find_element_by_id('search_form_input')
  13. assert search_input.get_attribute('value') == PHRASE

Все вызовы WebDriver делаются напрямую внутри тест-функции. Без комментариев этот код будет тяжело читать и понять. Вызовы WebDriver используют контр-интуитивные локаторы с низкоуровневыми командами взаимодействия. Тут отсутствует намерение – это тест поиска DuckDuckGo, а не набор кликов и ввода букв. К тому же локаторы веб-элементов могут дублироваться – например, локатор поисковой строки.

Паттерн Page Object, также известный как "модель Page Object" – это паттерн проектирования, который извлекает взаимодействия веб-страницы с целью улучшить их читабельность и повторное использование. Страницы представлены как классы с атрибутами локаторов и методами взаимодействия. Вместо сырых вызовов WebDriver тесты вызывают методы объекта страницы. Паттерн Page Object, в целом, наиболее распространенный паттерн Web UI-автоматизации. Существует множество способов внедрения этого паттерна, но по большей части они очень похожи.

Реструктуризация для Page Object

Наш тест взаимодействует с двумя страницами – страницей поиска DuckDuckGo и страницей результатов. Давайте напишем класс page object для каждой из них. Создайте новую директорию pages/ в корневом каталоге проекта, и добавьте пустой файл __init__.py, чтобы сделать ее пакетом. В этом пакете вам нужно создать два файла - search.py и result.py.

Директория вашего проекта должна выглядеть так:

  1. $ tree
  2. .
  3. ├── Pipfile
  4. ├── Pipfile.lock
  5. ├── pages
  6. │ ├── __init__.py
  7. │ ├── result.py
  8. │ └── search.py
  9. └── tests
  10. ├── test_math.py
  11. └── test_web.py

Размещение относящихся к тесту модулей вне директории tests/ может показаться странным, однако помните, что pytest рекомендует не делать пакетом модуль тестов. Создание отдельного пакета для Page Object позволяет тест-модулям легко их импортировать. Это также усиливает разделение тест-кейсов и веб-взаимодействий. Такой компоновки директорий достаточно для нашего проекта, однако другие (особенно крупные) проекты могут потребовать другого размещения. Пожалуйста, ознакомьтесь с руководством pytest по хорошим интеграционным практикам для получения более подробной информации.

Страница поиска

Страница поиска довольно проста. Наш тест взаимодействует с ней двумя способами – загружает и вводит поисковый запрос. Единственный локатор – строка поиска. Добавьте код в файл pages/search.py:

  1. from selenium.webdriver.common.by import By
  2. from selenium.webdriver.common.keys import Keys
  3. class DuckDuckGoSearchPage:
  4. URL = 'https://www.duckduckgo.com'
  5. SEARCH_INPUT = (By.ID, 'search_form_input_homepage')
  6. def __init__(self, browser):
  7. self.browser = browser
  8. def load(self):
  9. self.browser.get(self.URL)
  10. def search(self, phrase):
  11. search_input = self.browser.find_element(*self.SEARCH_INPUT)
  12. search_input.send_keys(phrase + Keys.RETURN)

Несмотря на небольшой размер, этот класс – хороший пример того, как должен выглядеть Page Object. Его имя, DuckDuckGoSearchPage, уникально и внятно определяет страницу. В нем есть атрибуты локаторов (SEARCH_INPUT), инициализатор (__init__) и методы взаимодействия (load и search). Рассмотрим каждую часть отдельно.

Атрибуты локаторов

Единственный локатор в этом классе – это SEARCH_INPUT для поиска поля ввода поискового запроса. Это атрибут класса ("статический"), потому что значение должно быть одинаковым для всех страниц поиска. Он также написан как кортеж, потому что все локаторы состоят из двух частей: типа (вроде By.ID или By.XPATH) и запроса. Этот локатор находит элемент строки поиска по имени. Создание локаторов как кортежей атрибута класса делает их читабельными и доступными. Локаторы также всегда должны иметь интуитивно понятные имена.

Инициализатор

Всем Page Object необходима ссылка на копию WebDriver. Обычно она внедряется через конструктор, а здесь – передается в метод __init__ как параметр браузера, а затем хранится как атрибут self.browser. Инъекция зависимости позволяет Page Object полиморфически использовать любой тип WebDriver – неважно, ChromeDriver, IEDriver, или нечто иное. Она также позволяет тест-фреймворку контролировать настройку и очистку.

Методы взаимодействия

Методы взаимодействия должны иметь интуитивно понятные имена. Метод load переводит браузер на страницу поиска. Заметьте, что URL – это атрибут класса. Метод search вводит поисковый запрос в поле ввода, но теперь фраза параметризована – можно использовать любую фразу.

Метод search также находит целевой элемент новаторским путем. Вместо использования метода find_element_by_name он пользуется более общим методом find_element, принимающим два аргумента – тип локатора и запрос. Эти аргументы соответствуют нашему кортежу SEARCH_INPUT. Оператор * расширяет self.SEARCH_INPUT до позиционных параметров для вызова метода. Здорово! Лично я люблю этот паттерн за то, что кортежи локаторов можно менять, и это не затронет код взаимодействия.

Рефакторинг поиска

Давайте переработаем шаги тест-кейса, включив наш новый Page Object поиска. Замените эти строки:

  1. URL = 'https://www.duckduckgo.com'
  2. PHRASE = 'panda'
  3. browser.get(URL)
  4. search_input = browser.find_element_by_id('search_form_input_homepage')
  5. search_input.send_keys(PHRASE + Keys.RETURN)

На новые вызовы page object:

  1. PHRASE = 'panda'
  2. search_page = DuckDuckGoSearchPage(browser)
  3. search_page.load()
  4. search_page.search(PHRASE)

Намного лучше! Это куда больше похоже на шаги тест-кейса, а не на программные вызовы.

Страница результатов

Теперь давайте напишем Page Object страницы результатов. Добавьте код в pages/result.py:

  1. from selenium.webdriver.common.by import By
  2. class DuckDuckGoResultPage:
  3. LINK_DIVS = (By.CSS_SELECTOR, '#links > div')
  4. SEARCH_INPUT = (By.ID, 'search_form_input')
  5. @classmethod
  6. def PHRASE_RESULTS(cls, phrase):
  7. xpath = f"//div[@id='links']//*[contains(text(), '{phrase}')]"
  8. return (By.XPATH, xpath)
  9. def __init__(self, browser):
  10. self.browser = browser
  11. def link_div_count(self):
  12. link_divs = self.browser.find_elements(*self.LINK_DIVS)
  13. return len(link_divs)
  14. def phrase_result_count(self, phrase):
  15. phrase_results = self.browser.find_elements(*self.PHRASE_RESULTS(phrase))
  16. return len(phrase_results)
  17. def search_input_value(self):
  18. search_input = self.browser.find_element(*self.SEARCH_INPUT)
  19. return search_input.get_attribute('value')

DuckDuckGoResultPage чуть-чуть сложнее DuckDuckGoSearchPage. Локатор LINK_DIVS следует паттерну кортежа, но локатор PHRASE_RESULTS ему не следует. Вместо этого он использует метод класса, возвращающий кортеж, и в результате поисковый запрос в его XPath можно параметризировать. Инициализатор тот же самый. Три метода взаимодействия находят элементы и возвращают значения.

Заметьте, что методы взаимодействия не задают правил, а просто возвращают состояния. Правила – проблема тест-кейса, а не Page Object. Разные тесты могут использовать вызовы одних и тех же page object для проверки различных типов правил.

Настало время для дополнительной доработки. Замените эти строки тест-функции:

  1. link_divs = browser.find_elements_by_css_selector('#links > div')
  2. assert len(link_divs) > 0
  3. xpath = f"//div[@id='links']//*[contains(text(), '{PHRASE}')]"
  4. phrase_results = browser.find_elements_by_xpath(xpath)
  5. assert len(phrase_results) > 0
  6. search_input = browser.find_element_by_id('search_form_input')
  7. assert search_input.get_attribute('value') == PHRASE

На эти:

  1. result_page = DuckDuckGoResultPage(browser)
  2. assert result_page.link_div_count() > 0
  3. assert result_page.phrase_result_count(PHRASE) > 0
  4. assert result_page.search_input_value() == PHRASE

Вау! И снова вышло гораздо чище.

Перезапуск теста

tests/test_web.py теперь должен выглядеть так:

  1. import pytest
  2. from pages.result import DuckDuckGoResultPage
  3. from pages.search import DuckDuckGoSearchPage
  4. from selenium.webdriver import Chrome
  5. @pytest.fixture
  6. def browser():
  7. # Инициализация ChromeDriver
  8. driver = Chrome()
  9. # Неявное ожидание готовности элементов перед попыткой взаимодействия
  10. driver.implicitly_wait(10)
  11. # Возвращение объекта драйвера в конце настройки
  12. yield driver
  13. # Для очистки покиньте драйвер
  14. driver.quit()
  15. def test_basic_duckduckgo_search(browser):
  16. # Настройте данные для тест-кейса
  17. PHRASE = 'panda'
  18. # Поиск фразы
  19. search_page = DuckDuckGoSearchPage(browser)
  20. search_page.load()
  21. search_page.search(PHRASE)
  22. # Проверка, что результаты появились
  23. result_page = DuckDuckGoResultPage(browser)
  24. assert result_page.link_div_count() > 0
  25. assert result_page.phrase_result_count(PHRASE) > 0
  26. assert result_page.search_input_value() == PHRASE

Перезапустите тест, чтобы убедиться, что он работает:

  1. $ pipenv run python -m pytest
  2. ============================= test session starts ==============================
  3. platform darwin -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 9 items
  6. tests/test_math.py ........ [ 88%]
  7. tests/test_web.py . [100%]
  8. =========================== 9 passed in 5.68 seconds ===========================

Супер! Все работает.

Больше тестов!

Две последние части руководства прояснили тонкости того, что изначально казалось простеньким Web UI-тестом. Мы не только автоматизировали взаимодействия для поиска DuckDuckGo – мы сделали это, используя лучшие практики и паттерны дизайна! Теперь то, чему мы научились, можно применять в новых тестах. Вот какие тесты еще можно написать:

  • Параметризировать тест для поиска других фраз.
  • Кликнуть по ссылке в результатах.
  • Искать изображения, видео и новости.

В следующий раз мы научимся конфигурировать выбор браузера и ожидания вне кода автоматизации, используя config-файлы.

Обсудить в форуме