Слушаем события в Selenium с помощью Listeners. Как реагировать на события без тонны кода

от автора

Привет!

Работа с веб-приложениями с использованием Selenium зачастую требует выполнения различных действий и обработки многочисленных событий. В стандартном подходе это может привести к написанию большого количества кода для логирования, обработки ошибок и выполнения других задач. В этой статье мы рассмотрим, как можно значительно упростить этот процесс, используя Listeners в Selenium.

Listeners позволяют «слушать» события, происходящие во время выполнения тестов, и реагировать на них, что делает код более чистым и управляемым. Они обеспечивают мощный механизм для автоматизации рутинных задач и улучшения удобочитаемости тестов.

Сразу отвечу на вопрос, чем отличаются декораторы от listeners.

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

Listeners, особенно в контексте Selenium, предназначены для «прослушивания» событий, происходящих во время тестирования. Они следят за такими событиями, как переход на новую страницу или клик по элементу, и автоматически выполняют определенные действия в ответ на эти события. Это упрощает управление тестами и уменьшает количество повторяющегося кода, делая его более чистым и удобным.

Listeners являются более специфичным инструментом, предназначенным конкретно под цели тестирования и упрощающим разработку проектов тестирования.


Немного про Listeners

В Selenium Listeners являются интерфейсами, которые позволяют «слушать» и реагировать на события, происходящие во время выполнения тестов. Эти события могут «слушать» разные действия, такие как клики по элементам, ввод текста, навигация между страницами и многие другие. Использование Listeners помогает упростить управление этими событиями, позволяя автоматизировать обработку без необходимости добавлять громоздкий код непосредственно в тестовые сценарии.

Примеры, как можно использовать listener:

  • Централизированное управление логированием. Вместо вставки логирования в каждом тесте, вы создаете один раз Listener, который будет логировать все необходимые события.

  • Дополнительная проверка перед действиями. Используя Listener, можно добавить дополнительные проверки перед выполнением определённых действий. Например, перед нажатием на кнопку, можно проверить, что все необходимые элементы на странице загружены.

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

  • Действия после завершения работы браузера. После завершения работы браузера, вы можете, например, использовать Listener для записи результатов тестирования (статус теста, время выполнения, ошибки, и т.д.) в базу данных. Это позволит вам централизованно хранить и анализировать результаты тестирования.

Полный набор функций для использования:

class AbstractEventListener:     """Event listener must subclass and implement this fully or partially."""      def before_navigate_to(self, url: str, driver) -> None:         pass      def after_navigate_to(self, url: str, driver) -> None:         pass      def before_navigate_back(self, driver) -> None:         pass      def after_navigate_back(self, driver) -> None:         pass      def before_navigate_forward(self, driver) -> None:         pass      def after_navigate_forward(self, driver) -> None:         pass      def before_find(self, by, value, driver) -> None:         pass      def after_find(self, by, value, driver) -> None:         pass      def before_click(self, element, driver) -> None:         pass      def after_click(self, element, driver) -> None:         pass      def before_change_value_of(self, element, driver) -> None:         pass      def after_change_value_of(self, element, driver) -> None:         pass      def before_execute_script(self, script, driver) -> None:         pass      def after_execute_script(self, script, driver) -> None:         pass      def before_close(self, driver) -> None:         pass      def after_close(self, driver) -> None:         pass      def before_quit(self, driver) -> None:         pass      def after_quit(self, driver) -> None:         pass      def on_exception(self, exception, driver) -> None:         pass

Напишем test-template с использованием listener

Начнем с базы — определим структуру проекта.

listenerExample/ │ ├── .venv/ │ ├── base/ │ ├── config/ │ ├── pages/ │ ├── tests/ │ ├── utils/ │ └── requirements.txt

Установим и импортируем необходимые библиотеки.
Напишем конфиг conftest.py для теста в директории tests. Этот код создаёт драйвер браузера для тестирования инициализирует его перед каждым тестом.

import pytest from selenium import webdriver   @pytest.fixture(params=["chrome"], scope="function") def driver(request):     options = webdriver.ChromeOptions()     options.add_argument("--headless=new")     options.add_experimental_option("prefs", {'profile.managed_default_content_settings.images': 2})      driver = webdriver.Chrome(options)     yield driver     driver.quit()

Определим ресурс с которым будем работать и определим ссылку до него в директории config
в файле links.py

home_page_url = "https://dostavka.dixy.ru"

В директории base напишем base_class.py для более удобного поиска веб-элементов.
Код создает базовый класс для работы с Selenium WebDriver, предоставляя методы для упрощённого поиска веб-элементов и проверки их состояния. Класс включает метод для получения стратегии поиска элементов по заданному типу, а также методы для ожидания видимости и кликабельности элементов.

from selenium.webdriver.common.by import By from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions as ex_con from selenium.webdriver.support.ui import WebDriverWait   class BaseClass:     def __init__(self, driver):         self.driver = driver         self.__wait = WebDriverWait(driver, 20, 1)      @staticmethod     def __get_selenium_by(find_by: str) -> str:         find_by = find_by.lower()         locating = {'css': By.CSS_SELECTOR,                     'xpath': By.XPATH,                     'class_name': By.CLASS_NAME,                     'id': By.ID,                     'link_text': By.LINK_TEXT,                     'name': By.NAME,                     'partial_link_text': By.PARTIAL_LINK_TEXT,                     'tag_name': By.TAG_NAME}         return locating[find_by]      def is_visible(self, find_by: str, locator: str = None, error_message: str = None) -> WebElement:         return self.__wait.until(             ex_con.visibility_of_element_located((self.__get_selenium_by(find_by), locator)),             error_message)      def is_clickable(self, find_by: str, locator: str = None, error_message: str = None) -> WebElement:         return self.__wait.until(             ex_con.element_to_be_clickable((self.__get_selenium_by(find_by), locator)),             error_message)

Напишем модель страницы в директории pages в файле home_page.py
Создаем класс локаторов для главной страницы, который предоставляет методы для взаимодействия с конкретными элементами. Также добавим простую проверку на соответствие цвета кнопки.

from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.color import Color  from base.base_class import BaseClass   class HomePageLocators(BaseClass):     __choose_delivery_address_button = "[test='body_header-main_buttons_container-delivery_address_selector']"     __map = "myMap"      def get_delivery_address_button(self, expected_color: str = '#000000') -> WebElement:         delivery_button = self.is_clickable("css", self.__choose_delivery_address_button)          actual_color = delivery_button.value_of_css_property("background-color")         actual_color_hex = Color.from_string(actual_color).hex         assert actual_color_hex == expected_color, f"Expected color {expected_color}, but got {actual_color_hex}"          return delivery_button      def get_map(self):         self.is_visible("id", self.__map) 

Для автоматического использования моделей страниц в тестах напишем base_pages.py, который будет содержать базовую настройку и инициализацию объектов страниц.

import pytest  from pages.home_page import HomePageLocators   class BasePage:     home_page: HomePageLocators      @pytest.fixture(autouse=True)     def pages(self, request, driver):         request.cls.home_page = HomePageLocators(driver) 

Создадим файл test_utils.py в директории utils с утилитами для теста.
Файл содержит в себе пару тривиальных проверок на доступность ресурса по url и проверки загруженности страницы.

import requests from selenium.webdriver.support.ui import WebDriverWait   def is_page_load(driver):     return WebDriverWait(driver, 20, 0.5).until(lambda d: d.execute_script('return document.readyState') == 'complete')   def check_url_status(url: str) -> None:     headers = {         'User-Agent': 'PostmanRuntime/7.39.0'     }     response = requests.get(url, headers=headers)     assert response.status_code == 200, f"Resource status code: {response.status_code}"

Теперь напишем listener

Создадим файл listener.py в директории tests.
Данный listener мониторит время загрузки страниц. Также, проверяет статус URL перед началом навигации и логирует время, затраченное на загрузку страницы, после завершения навигации.
До того, как мы перейдем на веб-ресурс мы проверим его доступность статус-кодом. А уже после того, как перейдем — проверим, что страница загрузилась полностью и мы можем начать тестирование.

import logging import time  from selenium.webdriver.support.events import AbstractEventListener  from utils.test_utils import is_page_load, check_url_status  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')   class MonitoringListener(AbstractEventListener):     def __init__(self):         self.start_time = 0.0         self.load_time = 0.0      def before_navigate_to(self, url: str, driver) -> None:         check_url_status(url)         self.start_time = time.time()         logging.info(f"Starting navigation to {url}")      def after_navigate_to(self, url: str, driver) -> None:         is_page_load(driver)         end_time = time.time()         self.load_time = end_time - self.start_time         logging.info(f"Page loaded in {self.load_time:.2f} seconds")

Добавим обертку для listener в conftest.py. Для того, чтобы обернуть конфиг нам понадобится EventFiringWebDriver и написанный ранее MonitoringListener .

import pytest from selenium import webdriver from selenium.webdriver.support.events import EventFiringWebDriver  from tests.listener import MonitoringListener   @pytest.fixture(params=["chrome"], scope="function") def driver(request):     options = webdriver.ChromeOptions()     options.add_argument("--headless=new")     options.add_experimental_option("prefs", {'profile.managed_default_content_settings.images': 2})      driver = webdriver.Chrome(options)     event_listener = MonitoringListener()     e_driver = EventFiringWebDriver(driver, event_listener)      yield e_driver     e_driver.quit()

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

from config.links import * from pages.base_pages import BasePage   class Test(BasePage):     def test_monitoring(self, driver):         driver.get(home_page_url)          self.home_page.get_delivery_address_button().click()         self.home_page.get_map()

На этот момент имеем следующую структуру проекта.

listenerExample/ │ ├── .venv/ │ ├── base/ │   └── base_class.py │ ├── config/ │   └── links.py │ ├── pages/ │   ├── base_pages.py │   └── home_page.py │ ├── tests/ │   ├── __init__.py │   ├── conftest.py │   ├── listener.py │   └── test_site.py │ ├── utils/ │   └── test_utils.py │ └── requirements.txt

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

От автора

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

Ссылочка на тележку


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


Комментарии

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

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