Есть отличная удаленная работа для php+codeception+jenkins+allure+docker спецов. 100% remote! Присоединиться к проекту

POM как инструмент быстрого и удобного написания UI-тестов на python

design-patterns
selenium
framework
python
page-object
architecture
webdriver
Теги: #<Tag:0x00007f7b627eb410> #<Tag:0x00007f7b627eb2d0> #<Tag:0x00007f7b627eb0f0> #<Tag:0x00007f7b627eaf10> #<Tag:0x00007f7b627eada8> #<Tag:0x00007f7b627eac68> #<Tag:0x00007f7b627eab28>

(Roma Marinsky) #21

А почему не вернуть просто объект ForgotPasswordPage

return new ForgotPasswordPage();

(Pavel Ponomaryov) #22

Самое смешное, когда я только начинал разбираться с Selenide и интересовался как инициализировать page объект в селениде мне указали такую конструкцию - page(MyPage.class) . Может для инициализации элементов с аннотациями @FindBy ? Не вникал - честно. Можно и просто написать return new ForgotPasswordPage().init()

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

[quote=“Roma_Marinsky, post:20, topic:11777”]
С инженерной точки зрения, качество проверки важнее читабельности сценария.
[/quote] Не соглашусь. При возросшем количестве тестов мне свои же тесты прочитать проще по этим простым шагам - ведь я уже через день могу не помнить, что за тесты написал. Это раз. Второй профит - фронтэнд разработчики могут взять мои спецификации (если говорить о веб приложении) и взять их на вооружение для своих тестов - подменяя реальные данные моками. В этом смысле feature файлы будут теми же - спецификация не меняется, но поменяется лишь имплементация шагов.


(Sergei Chipiga) #23

Честно говоря не вижу непрактичности, это нормальное явление в ООП - запрашивать принадлежащий (вложенный) объект как проперти. И это лучше чем создавать метод у объекта типа page, которого не должно быть. Контейнер не кликает по кнопке, у кнопки вызывается метод click. Это только кажется первоначально удобным, но на деле не соответствует архитектуре, Возникает правило - “добавлю методы куда хочу потому что мне сейчас это выгодно”, и про единообразие кода можно забыть. Если есть несколько строк кода, которые желательно выполнять вместе - их можно оформить как вспомогательный метод стэпа.

Приведу пример, показывающий что попытка добавить метод page.click_login() не скейлится. Н-р есть 10 страниц из 40 в проекте, у которых есть UI-элемент button_login, доступный через UI-форму form_login. Т.е. для одной страницы вызов будет в формате page_1.form_login.button_login.click(). Соответственно для каждой из 10 страниц обращение будет такое же в тех стэпах, где совершается клик по button_login.
Теперь если попытаться создать метод click_login(), кликающий внутри себя по кнопке, то нужно:

  • либо десять раз дублировать этот метод, и каждый раз копировать в новую page, где будет кнопка button_click.
  • либо сделать его в базовом классе, и при этом он будет распространен на все страницы, даже у которых этой кнопки нет, что совсем не ок (насколько я знаю джава не поддерживает множественное наследование и миксины, но и миксины бы тоже выглядели не к месту).

Если обращение page_1.form_login.button_login кажется длинным, а обращаться нужно несколько раз за стэп, можно сделать присвоение в переменную (локальную, либо доступную в инстансе класса стэпов).

Честно говоря не знаю, как SOLID связан с необходимостью в огромном количестве объектов (это больше определяется сложностью проекта). Если вы имеете ввиду signle responsibility, то кажется, лучше создать множество разных сравнительно небольших объектов, умеющих делать только то, что от них требуется (н-р CRUD/REST операции для одной сущности / ресурса).

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

P.S. Если говорить про иерархия-независимый доступ к UI-элементу внутри страницы, как вариант можно реализовать метод page.get_element('button_login') в базовом классе, который рекурсивно обойдет дерево зарегистрированных UI-элементов и вернет первый подходящий. Но я предпочитаю явное обращение к элементу, т.к. это не вовлекает строковую переменную и явно показывать иерархию обращений в коде.


(vmaximv) #24

Странный у вас “чат”. Топик-стартер реализовал HTMLElements на питоне, а в него кьюкумбером тычут :slight_smile:
Там и так есть за что “попинать”:


(Sergei Chipiga) #25

И вы конечно расскажете за что же таки?


(vmaximv) #26

Так я уже показал же. Бритва Оккама против классов ради классов.


(Sergei Chipiga) #27

Расскажите пжста подробнее, вас смущает что отнаследован класс от класса, который ничего нового не имплементит?


(vmaximv) #28

Меня смущает их ничтожность и бесполезность. Хотя если мне будут платить за количество строк кода - я, наверное, готов поменять свое мнение.


(Sergei Chipiga) #29

:slight_smile: постараюсь вас разубедить.

К сожалению мне за него никто не платит. Но про поменять мнение понравилось, видно что с вами можно договориться о цене)).

Ну ладно от шуток к делу. Почему так делается:

  • В питоне это можно встретить довольно часто, н-р при создании исключений:
class MyException(Exception):
      pass
  • Специализировать класс важно, чтобы уметь определить тип объекта в дальнейшем при использовании, н-р:
isinstance(ui_element, ui.Button)
  • Это также важно для расширяемости, потому что если в будущем понадобится расширить класс Button новыми методами, либо переопределить уже существующие - в проекте использующем эту либу, не понадобится делать изменений существующего кода.

  • Ну и немного перефразируя утиный тест: “Объект крякает как утка, плавает как утка и выглядит как утка. Давайте для него сделаем отдельный класс Утка”


(Pavel Ponomaryov) #30

Если уж на то пошло, можно не считать мои page objects таковыми. Пусть они будут называться page actions. Да, они хранят в себе элементы, ну и пусть. Если их будет слишком много - можно вынести в отдельный “контейнер” страницы, хотя чаще всего это не нужно. Благодаря этой архитектуре я могу в шагах вызывать методы последовательно аля
шаг 1 - выбрать элемент из дропдауна: page.expandDropdown().selectDropdownItem('my item'); шаг 2 - проверить наличие текста 'test' в элементе: page.elementX().shouldHave(exactText('test')).

Для более сложных элементов
page.showPopover().popover().funkyElement().shouldHave(text('foo'))

, где popover() возвращает класс контейнера для элемента popover с вложенными элементами;


(vmaximv) #31

– Кастомные эксепшены (и язык тут непричем) - это для https://en.wikipedia.org/wiki/Exception_handling

– Я возьму base.py и не увижу разницы

– Там нет кода => нечего менять.

Дальше спорить на эту тему бесполезно. Я не вижу смысла когда “типизация” существует ради “типизации”.
Вам не кажется, что это “слегка” не DRY?


(Sergei Chipiga) #32

Да согласен, что ваши страницы больше похожи на page actions.

C удобством использования по факту - это безусловно понятно. Подскажи как выделаете, когда н-р dropdown есть у нескольких страниц, а у большинства нет? Копируете метод expandDropdown в каждую страницу, или реализуете у базового класса?

И если есть н-р элемент emailField, вложенный внутри формы на странице, делаете ли для него методы clickEmailField, doubleClickEmailField, setEmailFieldValue, getEmailFieldValue и т.п. ?


(Sergei Chipiga) #33

Как я вам уже сказал, разница в том, что button - это не common UI элемент, а именно Button. И крайне желательно сделать для него класс, чтобы не впороться в дальнейшем как минимум н-р в выражении isinstance(ui_element, ui.Button), поскольку под выражение isinstance(ui_element, ui.UI) подходят все.

Это вполне очевидно. Однако, когда появится, то при вашем подходе, менять придется чрезвычайно много, т.к изменится поведение элемента типа Button, который почему-то в коде тестов определен как BaseUI. Дешевле решить проблему добавлением одного явного класса сразу, а не проводить потом рефакторинг.

Странно, что вы считаете кастомный эксепшен нормальным явлением, а кастомный класс для кастомного элемента - нет. Дело тут не в Exception_handling, а в том, что нужно отличать реальные объекты от базовых (абстрактных, хотя технически питон не включает понятие abstract class).

Кажется очень даже полезно, я бы с интересом также ознакомился с вашим кодом, который вы пишете. Если не затруднит, не подкинете ссылок на репозитории с вашим участием?

И тем не менее она есть, и имеет вполне технические обоснования, как было показано выше.

То, что вы приводите - это отчасти верное замечание, потому что на деле нужно было мне не заморачиваться с @property.setter, а сделать через функцию, потому что такой вариант http://paste.openstack.org/show/589854/ работать не будет. Можно было поколдовать и над геттором в попытке натянуть такой вариант с наследованием, но это все равно выглядело бы как хак. Мне было лень, оно работает сейчас, используется в проекте - менять в базовой реализации - отхватить головную боль на рефакторинг работающего кода.


(Pavel Ponomaryov) #34

конкретно в нашем случае dropdownы есть статичные - присутствуют на всех страницах и свойственные конкретным страницам. Статичные реализованы как отдельные классы. Их я и инициализирую как отдельные объекты. Остальные дропдауны отличаются друг от друга и поэтому реализованы по разному на каждой странице как и их методы expand.

Конечно же, если есть простой элемент emailField, вложенный внутри формы на странице я лишь реализую метод emailField(), возвращающий SelenideElement у которого есть стандартные методы - click(), doubleClick() и тд. Как говорится - зачем вдаваться в крайности, если можно применить смешанный подход. В зависимости от сложности элемента будет отличаться и реализация интеракций.


(Roma Marinsky) #35

А вот это уже проблема архитектурная, поскольку многие специалисты зациклились на “паттерне” page object, который усложняет соответствующие операции. А ведь проще иметь декомпозицию страниц на нужные для тестов блоки. К примеру попап/форма авторизации/регистрации может появиться на любой странице, ведь лучше создать самодостаточный объект для работы с этим попапом/формой и обращаться к нему на прямую в тесте, а не через объект страницы. Я как раз таки буду делать доклад по этому поводу во Львове. О проблемах мышления только через объекты страницы ко всем формам/полям на ней. А не через декомпозировнные объекты страниц, которые (если) повторяются на разных страницах


(Roma Marinsky) #36

Это всё слишком сложно, зачем такая многоходовка для простого экшона или проверки… Это слишком сложный хардкод. Если элемент кастомный и этот элемент не один, и эти элементы возможно в каком-то логическом блоке агрегированы. То лучше создать соответствующий объект этому блоку кастомных элементов. К примеру:

class CustomDropDown {

public CustomDropDown (){
    find("#customElement").shouldBe(visible);
}

public selectByText(String text){
    find("#customElement").click();
    find("#customElement input").sendKeys("text"+ Keys.ENTER);
}
public selectByValue(String value){
    find("#customElement").find("li[value='"+value+"']").scrollTo().click();
}

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


(Pavel Ponomaryov) #37

не совсем понял в чём тут отличие? Я же тоже самое написал? У меня только popover посложнее и содержит в себе ещё элементы. Тут варианты разные могут быть


(Roma Marinsky) #38

Отличия в следующем:

  1. Обращаешься к бизнес действию ты через страницу, а не через объект форма/попап - из которых состоят страницы/бизнес действия
  2. Метод selectDropdownItem зависит от expandDropdown, хотя это должен быть один метод selectDropdownItem в котором уже по умолчанию содержится раскрытие списка, если нужно.

И просуммировав отличия, можно получить то что я привёл в примере выше. Поскольку find("#customElement").click(); это и есть твой expandDropdown.
Лучше не кодировать в отдельный самостоятельный метод каждый шаг тест-кейса, потому что в тест-кейсах дублироваться будут эти шаги для манипуляции над кастомным элементом. К примеру для датапикера ты же не будешь писать длинную лапшу из: поискЭлемента.раскрытьДатапикер().найтиЧисло().клик();, ты сделаешь скорее всего setDate(), в котором и будут поиски, раскрытия, клики


(Pavel Ponomaryov) #39

не, ну я привёл лишь пример реализации. я могу и напрямую к дропдауну обратиться создав new MyDropdown(), только тут видишь некоторые товарищи против того, чтобы пихать несколько действий в один метод. В итоге, кстати всё равно придётся expand реализовывать для других тестовых сценариев, где тебе, например, надо проверить, что дропдаун закрывается при клике на другой элемент страницы. Тогда ты будешь делать примерно так : dropdown = new MyDropdown().expand(); page.Title().click(); assertThat(dropdown.expanded(), is(false));

И кстати, мне проще через объект страницы обращаться к её элементам, чем создавать новый объект. В этом смысле мне как раз “лапша” больше нравится.


(Roma Marinsky) #40

А зачем иметь отдельный тест на открытие, если этот функционал покрывается чуть более расширенным сценарием, а именно выбором значения

Нет ну если ваще надо и приспичило такое сделать, ок сделать паблик метод для экспанда

И почему лапша это плохо, а потому что порог вхождения нового спеца увеличивается на примерно 5-10 минут при виде каждой лапшинки и ещё больше увеличивается этот период когда ему нужно использовать эти готовые методы