t.me/atinfo_chat Telegram группа по автоматизации тестирования

Помогите с реализацией Pege Object. В голове всё перемешалось. Где истина/ложь ?

design-patterns
pytest
page-object
selenium
webdriver
Теги: #<Tag:0x00007f9e44e986f0> #<Tag:0x00007f9e44e98588> #<Tag:0x00007f9e44e98448> #<Tag:0x00007f9e44e982e0> #<Tag:0x00007f9e44e981a0>

(Dmitry Semenov) #1

Здравствуйте.
Данное сообщение, хоть и является вопросом, который основан на моём неведении, возможно, будет полезен кому-то из новичков как и я.
Текста будет много ( я не лаконичен ), потому, буду благодарен человеку, который всё-таки уделит время.
Обращаюсь к людям, которые имеют практику в реальных проектах. К сожалению, все отказываются принять стажёром,чтобы укрепить свои знания и понять грамотную реализацию qa на исходнках в боевых условиях :frowning:

Проблема в том, что недавно начал изучать selenium и pytest и наткнулся на такое понятие как Page Object. Без осознания как устроен данный паттерн, я не могу продолжить своё обучения.
Не из-за того, что оно нужно для изучения selenium, а из-за того, что это основа, на которой всё должно быть устроено у человека, который хочет чтобы его труд не был напрасен.

Я попробую реализовать данный паттерн на pytest и selenium и столкнулся с проблемами. Но прежде, буду благодарен за проверку моего понимания данного паттерна:

  1. У нас есть базовый класс, который имеет только одну функцию: функцию инициализации __init__ или конструктор, если мы говорим о другом языке C++, JAVA, C#. Данный метод класса привязывает драйвер браузера к переменной втурни него.

  1. Page Object - это представление одной и только одной страницы в виде потомка базового класса.

  1. При переходе на другую страницу, в рамках одного теста, мы передаём наш драйвер в качестве аргумента в Page Object соответствующей страницы и работает дальше с ним.

  1. Каждый Page Object имеет методы, которые описывают его работу со страницей, которую он представляет и локаторы элементов,которые на ней находятся. Ничего больше.

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

  1. Класс браузера. Я его объявил в качестве фикстуры для того, чтобы он был доступен везде, без import. Добавляю его в conftest через pytest_plugins = ['Driver'] Почему так ? Главный conftest не засоряется и вся работа с драйвером браузера в одном файле
  2. Page Object страницы. Внутри него, как я писал выше, методы работы внутри конкретной страницы.
Пример:
def open(self):
       #self.driver.maximize_window()
       self.driver.get('http://google.com')                
       assert self.driver.title == 'Google', 'Page not found?
  1. Локаторы и создание элемента. !Это самый проблемный вопрос! Как они реализованы у меня: Есть класс базовых элементов. Вот определение и небольшой кусочек
BaseElement.py
class BaseElement:
    
    def __init__(self,driver,locator):
        self.driver = driver
        self.locator = locator
        self.web_element = None
        self.find()

    def find(self):
        element = WebDriverWait(self.driver,10).until(
            EC.visibility_of_element_located(locator=self.locator))
        self.web_element = element
        return None
    
    def send_keys(self,value):
        self.web_element.send_keys(value)
        return None

Как инициализирую элемент интерфейса внутри Page Object конкретной страницы:

    @property
    def searchField(self):
        locator = find(By.CSS_SELECTOR, 'input[name="q"]')
        return BaseElement(self.driver, locator=locator)

@property для того, чтобы убрать необходимость в ()
Пример:
с @propety: main_page.searchField.send_keys('ny times')
без @property: main_page.searchField().send_keys('ny times')

также есть файл 

Find.py
from collections import namedtuple

find = namedtuple('find',['by','value'])

Для того, чтобы аргументы были 
find(By.CSS_SELECTOR, 'input[name="q"]')
a не
find(by=By.CSS_SELECTOR,value = 'input[name="q"]')
  1. +В чём я вижу плюсы данного метода:
    3.1. Все методы по работе с элементом внутри одного класса.
    3.2. Легко объявляются и проверяются при создании на отображение на странице.
    -!Минусы и вопросы! :
    3.3 Принимает только 1 элемент. со списками не работает. Стоит ли рассматривать реализацию решения этой проблемы , как это сделано в webium ? Созданием класса-потомка от BaseElement с переопределенной функцией поиска ?
    3.4 Т.к определение элементов находятся вместе с методами, то наш Page Object страницы уверено растёт. Надо ли выводить все локаторы в отдельный файл, потом передавать их как аргумент при создании элемента? Как мне кажется это глупо. Придётся работать с двумя файлами отдельно. Также, если они указаны внутри Page Object страницы, мы сразу видим и определение и локатор
    Правильно ли всё это ?

webium - пример реализации паттерна от wargaming. Если подумать, он хорош. Но в нём, при создании элемента, не проверяется его наличие на странице. Не проблема, можно добавить. Но получаем другую проблему:
Т.к мы хотим дать всем элементам, которые нам нужны - свои элементы класса BaseElement. Мы никогда не сможем получить title страницы вышеописанными методами.
Пример:

MainPage.py  
  @property
    def title(self):
        locator = find(By.CSS_SELECTOR, 'input[name="q"]')
        return BaseElement(self.driver, locator=locator)

    def isAt(self):
        assert self.title.text == '- Поиск в Google'

test_mainpage.py
class mainpageTests(object):
     def test_main_page_loaded(self,Driver):
          main_page = MainPage(Driver)
          main_page.open()
          main_page.isAt()


Т.к элемент title будет ожидаться через EC.visibility_of_element_located,
который мы указали в методе BaseElement,который ищет элемент на странице, он никогда не будет найден.
Можно решить через:

 def isAt(self):
     assert self.driver.title == 'Google'

Но насколько это правильно ? Ведь мы ушли от нашего правила элемент на странице = экземпляр класса BaseElement
Можно, конечно, добавить 3й аргумент, который будет указывать тип элемента. который мы ищем.
И в методе BaseElement,который ищет элемент на странице, через if/else указывать нужный нам expected_conditions для данного элемента, в случае, если он вообще нам нужен.

Это наверное самые беспокоящие меня проблемы, которые не дают мне покоя и возможности понять данный паттерн.
Не исключаю, что я слишком парюсь о чем-то. Но так я устроен, что я не могу считать, что я понимаю что-то , пока не реализую это в виде, пригодном для использования. Можно сделать и так и сяк, но как же всё таки правильно ?
Буду действительно благодарен человеку, который подскажет ,скажем так “Best practice”, правильно ли я вижу паттерн и его реализацию и поможет с пониманием моих проблем в любом удобном для Вас формате. ( устный, письменный литературный, либо готовый шаблон или просто куски кода на любом языке ).
С уважением, Дмитрий.


#2

Я не гуру в пейдж обжекте и это чисто мое мнение.
Представте, что у вас есть класс с тестами в котором около 20 тестов.
Все тесты для одной веб страницы. Теперь представим, что некоторые тесты используют одни и те же веб элементы - в таком случае будет логичным вынести эти элементы в отдельные поля. Дальше у нас в некоторых тестах есть повторяющиеся действия (а значит и код) - мы их тоже можем вынести в отдельные методы, так же удобней для поддержки ))
А теперь если взглянуть на наш класс, то мы увидим, например, что там 20 полей с локаторами, 20 тестов и 15 методов с повторяющимися действиями (которые мы вынесли). Читабельность такого кода слегка не очень. Для удобства мы можем все поля и вспомогательные методы вынести в отдельный класс, и в старом классе у нас будут только тесты, а в новом поля с локаторами и действия, которые мы из тестов вынесли в отдельные методы.
По моему это и есть паттерн пейдж обжект.

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