Делаем PageObject правильно. Удешевляем тестирование и снижаем порог вхождения

от автора

Привет, меня зовут Коля, занимаюсь тестированием 7 лет, автоматизацией — 6 лет. Так уж сложилось, что не особо люблю WebUI-тесты, но почему-то именно они у меня получаются лучше всего.

В один день мне позвонил мой друг Рома и сказал: «Коля, помнишь ты у нас автотесты делал с селениумом? Помоги мне сделать так же красиво».

Отправив Роме ссылку на свой гитхаб и устроив пару созвонов с объяснениями, что и как работает, я добился от него заветного «Я понял». 

Спустя два дня Рома позвонил снова: «Коля, я всё понял, а вот ребята в моей команде — нет. Помоги мне объяснить им».

Внутри я расскажу от том, как:

  • ускорить написание тестов;

  • снизить затраты на их поддержку;

  • прокачаться в написании фреймворков;

  • сэкономить деньги компании;

  • сохранить нервы сотрудников.

Что внутри статьи

В статье будет описана эволюция фреймворка для тестов, использующего:

  • Python;

  • Pytest;

  • Selenium (версии 3).

Использование Selenium версии 3.* обусловлено тем, что Selenium 4.*-вебдрайвер не дружит с синглтоном. На мой взгляд, причин для перехода с 3 на 4 нет совсем — изменения только в работе с Selenium Grid, который во всём проигрывает Selenoid. Если есть желание использовать новое-новое, то лучше посмотреть в сторону Playwright — с небольшими изменениям код из статьи можно адаптировать и под него.

Двигаться по статье будем так:

  1. пишем работающий код;

  2. разбираем, почему код выглядит плохо;

  3. улучшаем код;

  4. GOTO 1.

Установить всё необходимое поможет этот набор статей — Selenium для Python. Глава 1. Установка / Хабр (habr.com)

Статья рассчитана на опытного специалиста, который уже знает такие слова, как xpath, selenium webdriver, и уже имел опыт с Pytest, поэтому я пропущу объяснения основ.

Пишем тесты

Для примера возьмём два простых сценария:

  1. зайти на habr.com, проверить, что мы есть на хабре;

  2. зайти на habr.com, сделать поиск по фразе “ozon tech”, проверить, что все статьи содержат фразу “ozon tech”.

Здесь будет много кода, потому что это отправная точка всей статьи.

requirements.txt
selenium==3.141.0 pytest==7.4.3 urllib3<=2.0

Заметка — requirements.txt будет упомянут всего два раза. Фиксированные версии библиотек необходимы для возможности повторить результат на каждом этапе статьи.
Использование urllib3 <= 2.0 требуется только для запуска selenium webdriver.
Установка зависимостей происходит командой pip install -r requirements.txt.

driver.py
from selenium.webdriver import Firefox   class Driver(Firefox):     def __init__(self, *args, **kwargs):         super().__init__(*args, **kwargs)         self.implicitly_wait(5)  # default time waiting for a locator

conftest.py
import pytest  from driver import Driver   @pytest.fixture() def browser() -> Driver:     driver = Driver(executable_path="path/to/driver")     driver.set_window_size(1920, 1080)     driver.get("https://habr.com/ru/all")      yield driver      driver.quit()

test_ozon.py
from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions from selenium.webdriver.support.wait import WebDriverWait  from driver import Driver   def test_main_page(browser: Driver):     browser.find_element_by_xpath('//a[contains(@class, "tm-header__logo")]')     browser.find_element_by_xpath('//a[contains(@class, "tm-header-user-menu__search")]')     browser.find_element_by_xpath('//section[@id="news_block_1"]')   def test_search(browser: Driver):     search_string = "ozon tech"      open_search = browser.find_element_by_xpath('//a[contains(@class, "tm-header-user-menu__search")]')     open_search.click()      search_input = browser.find_element_by_xpath('//div[contains(@class, "tm-search__input")]//input')     search_input.send_keys(search_string)      search_button = browser.find_element_by_xpath('//div[contains(@class, "tm-search__input")]//span')     search_button.click()      WebDriverWait(browser, 10).until_not(expected_conditions.presence_of_element_located((By.XPATH, '//div[contains(@class, "placeholder-wrapper")]')))      browser.find_element_by_xpath('//a[contains(@class, "tm-header__logo")]')     browser.find_element_by_xpath('//a[contains(@class, "tm-header-user-menu__search")]')      articles = browser.find_elements_by_xpath('//div[@class="tm-articles-list"]/article')     assert articles, "empty articles list"      errors = []     for article in articles:         if search_string not in article.text.lower():             errors.append(f"'{search_string}' not found in '{article.text.lower()}'")      assert not errors, errors

Добавляем PageObject

Тесты написаны, но выглядят как-то не так, очевидное улучшение, которое можно сделать — добавить переиспользуемость кода из тестов.

Когда мы говорим о тестах для веба, этот паттерн называется PageObject — взаимодействие со страницей и её возможностями на основе методов, которые есть у страницы.

Создадим новый класс страницы и поместим туда пару методов для работы с объектами страницы:

pages/main_page.py
from driver import Driver   class MainPage:     header_logo_xpath = '//a[contains(@class, "tm-header__logo")]'     header_search_xpath = '//a[contains(@class, "tm-header-user-menu__search")]'     search_input_xpath = '//div[contains(@class, "tm-search__input")]//input'     search_button_xpath = '//div[contains(@class, "tm-search__input")]//span'     news_block_xpath = '//section[@id="news_block_1"]'      @staticmethod     def verify_page(driver: Driver):         driver.find_element_by_xpath(MainPage.header_logo_xpath)         driver.find_element_by_xpath(MainPage.header_search_xpath)         driver.find_element_by_xpath(MainPage.news_block_xpath)      @staticmethod     def open_search(driver: Driver):         open_search = driver.find_element_by_xpath(MainPage.header_search_xpath)         open_search.click()      @staticmethod     def input_search(driver: Driver, search: str):         search_input = driver.find_element_by_xpath(MainPage.search_input_xpath)         search_input.send_keys(search)      @staticmethod     def press_search_button(driver: Driver):         search_button = driver.find_element_by_xpath(MainPage.search_button_xpath)         search_button.click()

pages/search_page.py
from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions from selenium.webdriver.support.wait import WebDriverWait  from driver import Driver   class SearchPage:     header_logo_xpath = '//a[contains(@class, "tm-header__logo")]'     header_search_xpath = '//a[contains(@class, "tm-header-user-menu__search")]'     article_xpath = '//div[@class="tm-articles-list"]/article'     preview_xpath = '//div[contains(@class, "placeholder-wrapper")]'      @staticmethod     def wait_for_loader_off(driver: Driver):         WebDriverWait(driver, 10).until_not(expected_conditions.presence_of_element_located((By.XPATH, SearchPage.preview_xpath)))      @staticmethod     def verify_page(driver: Driver, expected_string_in_results: str):         SearchPage.wait_for_loader_off(driver)          driver.find_element_by_xpath(SearchPage.header_logo_xpath)         driver.find_element_by_xpath(SearchPage.header_search_xpath)          articles = driver.find_elements_by_xpath(SearchPage.article_xpath)         assert articles, "empty articles list"          errors = []         for article in articles:             if expected_string_in_results not in article.text.lower():                 errors.append(f"'{expected_string_in_results}' not found in '{article.text.lower()}'")          assert not errors, errors

Применим наши изменения к тестам:

test_ozon.py
from driver import Driver from pages.main_page import MainPage from pages.search_page import SearchPage   def test_main_page(browser: Driver):     MainPage.verify_page(browser)   def test_search(browser: Driver):     search_string = "ozon tech"      MainPage.open_search(browser)     MainPage.input_search(browser, search_string)     MainPage.press_search_button(browser)     SearchPage.verify_page(browser, search_string)

Тесты выглядят намного проще, не так ли?

Приколачиваем PageElement

Стало лучше, но на разных страницах у нас есть пересечение по элементам. Посмотрим в будущее, сделаем отдельный класс для повторяющихся элементов, в нашем случае —  для Header.

Для этого воспользуемся паттерном PageElement — обычно он рядом с PageObject, если у вас есть повторяющиеся элементы на страницах.

Создадим новый класс компонента Header, добавим в него общие элементы.

components/header.py
from driver import Driver   class ComponentHeader:     header_logo_xpath = '//a[contains(@class, "tm-header__logo")]'     header_search_xpath = '//a[contains(@class, "tm-header-user-menu__search")]'      @staticmethod     def verify_component(driver: Driver):         driver.find_element_by_xpath(ComponentHeader.header_logo_xpath)         driver.find_element_by_xpath(ComponentHeader.header_search_xpath)      @staticmethod     def open_search(driver: Driver):         open_search = driver.find_element_by_xpath(ComponentHeader.header_search_xpath)         open_search.click()

Добавим новый компонент в страницы.

pages/main_page.py
from components.header import ComponentHeader from driver import Driver   class MainPage:     Header = ComponentHeader      search_input_xpath = '//div[contains(@class, "tm-search__input")]//input'     search_button_xpath = '//div[contains(@class, "tm-search__input")]//span'     news_block_xpath = '//section[@id="news_block_1"]'      @staticmethod     def verify_page(driver: Driver):         MainPage.Header.verify_component(driver)         driver.find_element_by_xpath(MainPage.news_block_xpath)      @staticmethod     def input_search(driver: Driver, search: str):         search_input = driver.find_element_by_xpath(MainPage.search_input_xpath)         search_input.send_keys(search)      @staticmethod     def press_search_button(driver: Driver):         search_button = driver.find_element_by_xpath(MainPage.search_button_xpath)         search_button.click()

pages/search_page.py
from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions from selenium.webdriver.support.wait import WebDriverWait  from components.header import ComponentHeader from driver import Driver   class SearchPage:     Header = ComponentHeader      article_xpath = '//div[@class="tm-articles-list"]/article'     preview_xpath = '//div[contains(@class, "placeholder-wrapper")]'      @staticmethod     def wait_for_loader_off(driver: Driver):         WebDriverWait(driver, 10).until_not(expected_conditions.presence_of_element_located((By.XPATH, SearchPage.preview_xpath)))      @staticmethod     def verify_page(driver: Driver, expected_string_in_results: str):         SearchPage.wait_for_loader_off(driver)          SearchPage.Header.verify_component(driver)          articles = driver.find_elements_by_xpath(SearchPage.article_xpath)         assert articles, "empty articles list"          errors = []         for article in articles:             if expected_string_in_results not in article.text.lower():                 errors.append(f"'{expected_string_in_results}' not found in '{article.text.lower()}'")          assert not errors, errors

Чуть-чуть поменяем тесты с учётом нового компонента.

test_ozon.py
def test_search(browser: Driver):     search_string = "ozon tech"      MainPage.Header.open_search(browser)     MainPage.input_search(browser, search_string)     MainPage.press_search_button(browser)     SearchPage.verify_page(browser, search_string)

Driver Driver Driver Driver. Убираем копипасту — добавляем Singleton

Нынешнее решение уже довольно хорошо выглядит — нет поисков локаторов в тестах, всё инкапсулировано в страницах. Страницы, в свою очередь, разделены на разные классы — удобно добавлять изменения в определённые страницы, не ломая в это время другие.

Можно ли что-то ещё добавить для улучшения? Можно. Но не добавить, а убрать — избавимся от вездесущей копипасты с запихиванием веб-драйвера в аргументы.

Браузер в тестах у нас всегда один, зачем его постоянно упоминать? Можно сделать Singleton, который будет возвращать наш браузер.

Мнение — я верю, что тесты с использованием селениума проверяют интерфейс, а не бэкенд. Если у вас возникает необходимость использовать две сессии одновременно, скорее всего, вы проверяете работу двух пользователей над одной сущностью, чего в большинстве случаев можно добиться, реализовав работу второго пользователя через бэкенд, не браузер.

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

Меняем код драйвера.

driver.py
from selenium.webdriver import Firefox   class Singleton(type):     _instances = {}      def __call__(cls, *args, **kwargs):         if cls not in cls._instances:             cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)         return cls._instances[cls]   class Driver(Firefox, metaclass=Singleton):      def __init__(self, *args, **kwargs):         super().__init__(*args, **kwargs)         self.implicitly_wait(5)  # default time waiting for a locator

После данных изменений ничего не запустится — особенности urlllib3 и selenium webdriver. Поднимем версию urllib3.

requirements.txt
selenium==3.141.0 pytest==7.4.3 urllib3==2.1.0

Обновим teardown в фикстуре и добавим autouse:

conftest.py
import pytest  from driver import Driver   @pytest.fixture(autouse=True) def browser() -> Driver:     driver = Driver(executable_path="path/to/driver")     driver.set_window_size(1920, 1080)     driver.get("https://habr.com/ru/all")      yield driver      try:         driver.quit()     finally:         driver.__class__._instances = {}

Уберём прокидывание Driver в методы компонента:

components/header.py
from driver import Driver   class ComponentHeader:     header_logo_xpath = '//a[contains(@class, "tm-header__logo")]'     header_search_xpath = '//a[contains(@class, "tm-header-user-menu__search")]'      @staticmethod     def verify_component():         Driver().find_element_by_xpath(ComponentHeader.header_logo_xpath)         Driver().find_element_by_xpath(ComponentHeader.header_search_xpath)      @staticmethod     def open_search():         open_search = Driver().find_element_by_xpath(ComponentHeader.header_search_xpath)         open_search.click()

Повторим для страниц:

pages/main_page.py
from components.header import ComponentHeader from driver import Driver   class MainPage:     Header = ComponentHeader      search_input_xpath = '//div[contains(@class, "tm-search__input")]//input'     search_button_xpath = '//div[contains(@class, "tm-search__input")]//span'     news_block_xpath = '//section[@id="news_block_1"]'      @staticmethod     def verify_page():         MainPage.Header.verify_component()         Driver().find_element_by_xpath(MainPage.news_block_xpath)      @staticmethod     def input_search(search: str):         search_input = Driver().find_element_by_xpath(MainPage.search_input_xpath)         search_input.send_keys(search)      @staticmethod     def press_search_button():         search_button = Driver().find_element_by_xpath(MainPage.search_button_xpath)         search_button.click()

pages/search_page.py
from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions from selenium.webdriver.support.wait import WebDriverWait  from components.header import ComponentHeader from driver import Driver   class SearchPage:     Header = ComponentHeader      article_xpath = '//div[@class="tm-articles-list"]/article'     preview_xpath = '//div[contains(@class, "placeholder-wrapper")]'      @staticmethod     def wait_for_loader_off():         WebDriverWait(Driver(), 10).until_not(expected_conditions.presence_of_element_located((By.XPATH, SearchPage.preview_xpath)))      @staticmethod     def verify_page(expected_string_in_results: str):         SearchPage.wait_for_loader_off()          SearchPage.Header.verify_component()          articles = Driver().find_elements_by_xpath(SearchPage.article_xpath)         assert articles, "empty articles list"          errors = []         for article in articles:             if expected_string_in_results not in article.text.lower():                 errors.append(f"'{expected_string_in_results}' not found in '{article.text.lower()}'")          assert not errors, errors

Почистим тесты от упоминания Driver:

test_ozon.py
from pages.main_page import MainPage from pages.search_page import SearchPage   def test_main_page():     MainPage.verify_page()   def test_search():     search_string = "ozon tech"      MainPage.Header.open_search()     MainPage.input_search(search_string)     MainPage.press_search_button()     SearchPage.verify_page(search_string)

Избавились от копипасты с webdriver, Driver, driver — теперь не нужно дополнительно везде добавлять его как параметр, достаточно в необходимом месте сделать вызов Driver.

Заворачиваем PageObject в PageObject

От копипасты избавились уже аж два раза, но что, если я скажу, что у нас всё ещё остаётся копипаста? А что, если мы представим, что каждый элемент DOM-дерева — это контейнер методов?

Спойлер — так оно и есть, надо только сделать удобный интерфейс для взаимодействия.

Попробуем сделать отдельный класс для элемента DOM-дерева. Пусть у него будет xpath и ссылка на webelement (объект в DOM-дереве на открытой драйвером странице).

locator.py
from selenium.webdriver.remote.webelement import WebElement   class Locator:     xpath: str = None     name: str = None     webelement: WebElement = None      def __init__(self, xpath: str, name: str):         self.xpath = xpath         self.name = name

Добавим метод для проверки наличия локатора на странице:

locator.py
def is_on_page(self) -> bool:     elements = Driver().find_elements_by_xpath(self.xpath)     if elements:         return True     return False   def wait_for_disappear(self):     WebDriverWait(Driver(), 10).until_not(expected_conditions.presence_of_element_located((By.XPATH, self.xpath)))   def get_all_elements(self):     response = []     for element in Driver().find_elements_by_xpath(self.xpath):         locator = Locator(xpath=self.xpath, name=self.name)         locator.webelement = element         response.append(locator)     return response

Добавим ✨магический✨ метод получения webelement:

locator.py
def _webelement_required(self):     def update_webelement():         try:             self.webelement = Driver().find_element_by_xpath(self.xpath)             return self         except Exception:             raise Exception(f'Error: cannot locate {self.name}')      # magic function to update webelement if there is no webelement or if DOM element changed     if not self.webelement:         update_webelement()         return     try:         # if not fails, webelement still has the DOM object link         self.webelement.location         return     except StaleElementReferenceException:         update_webelement()

Магия работает просто — если удалось обратиться к webelement, значит он ещё есть на странице. Если не удалось обратиться — значит DOM-дерево перестроилось, необходимо найти новый элемент.

Важно — не всем методам необходим webelement. Например, чтобы проверить, что элемент отображается на странице, нам необязательно привязываться к webelement, достаточно xpath-локатора.

Добавим интерактивных методов, таких как клик, получение и ввод текста:

locator.py
def click(self):     self._webelement_required()     self.webelement.click()  def text(self) -> str:     self._webelement_required()     return self.webelement.text  def input(self, value: str):     self._webelement_required()     self.webelement.send_keys(value)

Полный код locator.py:

locator.py
from __future__ import annotations  from selenium.common.exceptions import StaleElementReferenceException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions from selenium.webdriver.support.wait import WebDriverWait  from driver import Driver   class Locator:     xpath: str = None     name: str = None     webelement: WebElement = None      def __init__(self, xpath: str, name: str):         self.xpath = xpath         self.name = name      def _webelement_required(self):         def update_webelement():             try:                 self.webelement = Driver().find_element_by_xpath(self.xpath)                 return self             except Exception:                 raise Exception(f'Error: cannot locate {self.name}')          # magic function to update webelement if there is no webelement or if DOM element changed         if not self.webelement:             update_webelement()             return         try:             # if not fails, webelement still has the DOM object link             self.webelement.location             return         except StaleElementReferenceException:             update_webelement()      def is_on_page(self) -> bool:         elements = Driver().find_elements_by_xpath(self.xpath)         if elements:             return True         return False      def click(self):         self._webelement_required()         self.webelement.click()      def text(self) -> str:         self._webelement_required()         return self.webelement.text      def input(self, value: str):         self._webelement_required()         self.webelement.send_keys(value)      def wait_for_disappear(self):         WebDriverWait(Driver(), 10).until_not(expected_conditions.presence_of_element_located((By.XPATH, self.xpath)))      def get_all_elements(self) -> [Locator]:         response = []         for element in Driver().find_elements_by_xpath(self.xpath):             locator = Locator(xpath=self.xpath, name=self.name)             locator.webelement = element             response.append(locator)         return response

Рефакторим странички под новую реальность

Новая сущность — интерфейс для работы с xpath — готова, теперь надо применить её к существующему коду.

После недолгих манипуляций получаем следующую картину:

components/header.py
from locator import Locator   class ComponentHeader:     header_logo = Locator('//a[contains(@class, "tm-header__logo")]', "Header logo")     header_search = Locator('//a[contains(@class, "tm-header-user-menu__search")]', "Header search")      @staticmethod     def verify_component():         assert ComponentHeader.header_logo.is_on_page()         assert ComponentHeader.header_search.is_on_page()      @staticmethod     def open_search():         ComponentHeader.header_search.click()

pages/main_page.py
from components.header import ComponentHeader from locator import Locator   class MainPage:     Header = ComponentHeader      search_input = Locator('//div[contains(@class, "tm-search__input")]//input', "Search input")     search_button = Locator('//div[contains(@class, "tm-search__input")]//span', "Search button")     news_block = Locator('//section[@id="news_block_1"]', "News block")      @staticmethod     def verify_page():         MainPage.Header.verify_component()         assert MainPage.news_block.is_on_page()      @staticmethod     def input_search(search: str):         MainPage.search_input.input(search)      @staticmethod     def press_search_button():         MainPage.search_button.click()

pages/search_page.py
from components.header import ComponentHeader from locator import Locator   class SearchPage:     Header = ComponentHeader      article = Locator('//div[@class="tm-articles-list"]/article', "Article")     preview = Locator('//div[contains(@class, "placeholder-wrapper")]', "Article placeholder")      @staticmethod     def wait_for_loader_off():         SearchPage.preview.wait_for_disappear()      @staticmethod     def verify_page(expected_string_in_results: str):         SearchPage.wait_for_loader_off()          SearchPage.Header.verify_component()          articles = SearchPage.article.get_all_elements()         assert articles, "empty articles list"          errors = []         for article in articles:             if expected_string_in_results not in article.text().lower():                 errors.append(f"'{expected_string_in_results}' not found in '{article.text().lower()}'")          assert not errors, errors

Теперь методы внутри страниц являются однострочниками. Секундочку, это что получается, теперь можно не писать методы для каждой страницы, а просто обращаться к классу Locator?

Почистим код страниц ещё раз, а потом посмотрим на тесты.

components/header.py
from locator import Locator   class ComponentHeader:     header_logo = Locator('//a[contains(@class, "tm-header__logo")]', "Header logo")     header_search = Locator('//a[contains(@class, "tm-header-user-menu__search")]', "Header search")      @staticmethod     def verify_component():         assert ComponentHeader.header_logo.is_on_page()         assert ComponentHeader.header_search.is_on_page()

pages/main_page.py
from components.header import ComponentHeader from locator import Locator   class MainPage:     Header = ComponentHeader      search_input = Locator('//div[contains(@class, "tm-search__input")]//input', "Search input")     search_button = Locator('//div[contains(@class, "tm-search__input")]//span', "Search button")     news_block = Locator('//section[@id="news_block_1"]', "News block")      @staticmethod     def verify_page():         MainPage.Header.verify_component()         assert MainPage.news_block.is_on_page()

pages/search_page.py
from components.header import ComponentHeader from locator import Locator   class SearchPage:     Header = ComponentHeader      article = Locator('//div[@class="tm-articles-list"]/article', "Article")     preview = Locator('//div[contains(@class, "placeholder-wrapper")]', "Article placeholder")      @staticmethod     def verify_page(expected_string_in_results: str):         SearchPage.preview.wait_for_disappear()          SearchPage.Header.verify_component()          articles = SearchPage.article.get_all_elements()         assert articles, "empty articles list"          errors = []         for article in articles:             if expected_string_in_results not in article.text().lower():                 errors.append(f"'{expected_string_in_results}' not found in '{article.text().lower()}'")          assert not errors, errors

test_ozon.py
from pages.main_page import MainPage from pages.search_page import SearchPage   def test_main_page():     MainPage.verify_page()   def test_search():     search_string = "ozon tech"      MainPage.Header.header_search.click()     MainPage.search_input.input(search_string)     MainPage.search_button.click()     SearchPage.verify_page(search_string)

Как видим в примере тестов, инкапсуляция работы с селениумом позволяет убрать всю боль от работы с DOM внутрь фреймворка.

Результаты

Какие плюсы после всех этих улучшений:

  • быстрее внедрение новых возможностей во фреймворк;

  • проще и быстрее написание тестов за счёт уменьшения объёма кода;

  • проще и быстрее добавление новых страниц за счёт инкапсуляции работы с DOM;

  • снижение порога входа для начала автоматизации за счёт ограничения доступных методов для взаимодействия;

  • нужен минимум опытных людей для поддержки фреймворка.

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

Бонус — добавляем Allure

Как proof of concept лёгкости добавления новых возможностей во фреймворк сейчас мы добавим Allure-отчёты в наши тесты.

Установим Allure:

pip install allure-pytest

Добавим его использование в класс Locator:

locator.py
def click(self):     with allure.step(f"Click on {self.name}"):         self._webelement_required()         self.webelement.click()  def input(self, value: str):     with allure.step(f"Input '{value}' in {self.name}"):         self._webelement_required()         self.webelement.send_keys(value)  def wait_for_disappear(self):     with allure.step(f"Wait for disappear {self.name}"):         WebDriverWait(Driver(), 10).until_not(expected_conditions.presence_of_element_located((By.XPATH, self.xpath)))

Запустим тесты с генерацией отчёта:

pytest --alluredir=./allure_results

Восхитимся нашим отчётом:

allure serve allure_results/

Вышло довольно просто, не так ли?

В конце добавлю ссылку на свой гитхаб с чуть более развитым (и чуть более устаревшим) фреймворком — https://github.com/giant-whale/selenoid-python-framework

Надеюсь, эта статья поможет улучшить тестирование в вашей компании.

P.S. Рома, теперь ты можешь просто кинуть ссылку на эту статью своим коллегам, сомневающимся в полезности изменений.


ссылка на оригинал статьи https://habr.com/ru/articles/786000/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *