Привет!

Работа с веб-приложениями с использованием 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 может способствовать улучшению модульности тестов, поскольку функциональность, связанная с обработкой событий, отделена от основной логики тестов.

От автора

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

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

Комментарии (2)


  1. dboichenko1
    10.06.2024 22:40
    +2

    Спасибо!
    Хорошая статья. Средний уровень - разбирал код несколько часов (((
    Из улучшений - я бы добавил краткое описание для функций и классов внутри кода


    1. luffity Автор
      10.06.2024 22:40
      +1

      Принял к сведению :)
      Вот ссылочка на github с этим проектом - https://github.com/Revastein/listenerExample
      Удачи в изучении!)