Привет!
Работа с веб-приложениями с использованием 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
может способствовать улучшению модульности тестов, поскольку функциональность, связанная с обработкой событий, отделена от основной логики тестов.
От автора
В целом, использование 'слушателей' событий имеет место быть, так как является мощным специфическим инструментом тестирования. Они позволяют отслеживать и логировать различные действия, такие как клики, навигация и изменение состояния элементов, что существенно облегчает отладку и анализ тестов. Слушатели событий предоставляют массу применений, включая улучшение отчетности, автоматическое снятие скриншотов при ошибках и сбор метрик производительности, что делает их незаменимым инструментом с неограниченным потенциалом для повышения надежности и эффективности тестирования.
dboichenko1
Спасибо!
Хорошая статья. Средний уровень - разбирал код несколько часов (((
Из улучшений - я бы добавил краткое описание для функций и классов внутри кода
luffity Автор
Принял к сведению :)
Вот ссылочка на github с этим проектом - https://github.com/Revastein/listenerExample
Удачи в изучении!)