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

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

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

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();
}

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

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

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

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

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

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

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

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

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

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

Я привёл пример - тест не на открытие, а на закрытие. При выборе значения дропдаун может и закроется, а вот при клике вне дропдауна нет. Это разные сценарии. Возможно вы и правы насчёт порога вхождения - сложно судить, так как это вещь субъективная. В любом случае придётся вникать откуда что приходит и какие методы доступны через объект. А уж нажать шорткат в IDE и увидеть что там внутри того или иного метода творится не выходя из класса дело секундное.

Кстати мы тут оффтопим не по-детски уже

Та отлично похоливарили :slight_smile: ну а вот такие методы предпочитаю делать только по надобности и это точно буду паблик методы, которые могут/будут использоваться для комплексного бизнес сценария :slight_smile:

@Sergei_Chipiga а при написании этой библиотеки, смотрели на GitHub - wgnet/webium: Webium is a Page Object pattern implementation library for Python (http://martinfowler.com/bliki/PageObject.html). It allows you to extend WebElement class to your custom controls like Link, Button and group them as pages.?
Webium тоже делался как миграция HtmlElements на python.

Возможно, часть сущностей в POM лишние. Зачем нужен TestField, Button, UI если есть уже существующий WebElement?

Ну и в декораторы загонять всё как-то странно. Некоторые страницы имеют по несколько десятков полей. IDE не найдёт потом эти аттрибуты у класса.

@Igor, доброго дня,

Да я смотрел webium. Честно говоря мне код не понравился. Н-р из-за подобных вещей: webium/base_page.py at master · wgnet/webium · GitHub.

Существующий WebElement - это реализация selenium’a. Мне нужен был враппер, чтобы делать кэширование и отложенную инициализацию. Кроме того, хотелось спрятать низкоуровневые вызовы и предоставить высокоуровневый API. Если для работы с дефолтными полями и кнопками эти методы совпадают, то для сложных структур, типа листов, таблиц, деревьев, а также кастомных комбобоксов и других элементов, низкоуровневая логика будет весьма значительна в коде.

В декораторы можно все UI не загонять, а описывать их как проперти внутри класса (что посути и делает декоратор).

Любопытно сравнить библиотеки, значит.

Насколько кэш ускоряет тесты? Я не вижу проблем его добавить в Webium, но непонятно зачем. А отложенная инициализация - это как раз и есть то, как Webium работает.

код не понравился

Как в POM делается проверка на то, есть ли элемент?

По поводу сложных структур. Find — Webium 1.2.1 documentation - ну вот оно и есть. А зачем прятать низкоуровневый API WebElementа? При желании можно не дать его вызывать при наследовании конечно.

1 лайк

@Igor,
Я вечером напишу, что именно мне не нравится в webium (глобальный объект драйвера, проброс наружу низкоуровневых объектов, is_element_present как самостоятельная сущность и проч.) и отвечу на ваши вопросы и опишу, почему на мой взгляд POM удобнее. К сожалению сейчас нет возможности.

@Igor,

Доброго вечера, к сожалению вчера был не лучший день для код ревью :slight_smile:

Почему я предпочел написать POM, а не использовать webium для UI-автотестов.

  1. В целом код webium’a плохо документирован и додумывать за авторов совершенно не хочется. Это резко снижает желание юзать стороннюю либу и контрибьютить в нее, исправляя ошибки и добавляя новую функциональность. Проще сделаю свое под себя, а заодно прокачаться в построении архитектуры фреймворка (POM делался в свободное время).

  2. Архитектура webium’a на мой взгляд не полноценна. Сразу начинается с декларирования страницы (Welcome to Webium’s documentation! — Webium 1.2.1 documentation). Хотя страница - это лишь часть объекта-агрегатора страниц - приложения. Я считаю, что правильно выстраивать цепочку зависимостей Application → Pages → UI-elements. То что для работы с Application используется браузер - об этом у автоматизатора должно быть минимум знаний (лишь указать при инициализации application’a, какой браузер использовать, кое-какие настройки для него, URL доступа: pom/base.py at master · schipiga/pom · GitHub). Дальше должно быть управление только через сущность application (по этой причине кстати POM не поддерживает работу с несколькими страницами браузера. Я согласен, что это недоработка, т.к. многооконность неотъемлема при тестировании, и будет имплементирована позже когда остро понадобится).

  3. Implicit_wait, который использует webium. На мой взгляд - это одна из опасных вещей, которая есть в selenium’e. Implicit_wait работает всегда. Eсли нужно вдруг получить значение сразу н-р для element.is_present, приходится его отключать. Это порождает н-р такой код webium/no_implicitly_wait.py at master · wgnet/webium · GitHub. По-моему, куда логичнее и явнее по умолчанию возвращать результат вызова метода сразу, без неявного ожидания. А методы элемента, перед которыми требуется ожидание, нужно оборачивать в декоратор, который будет устанавливать время ожидания перед вызовом метода и сбрасывать после. Да, это приводит к дополнительным запросам к selenium’у, но ничего в этом страшного нет, селениум от этого не умрет, на скорость тестов это несильно повлияет. Стоящий прирост скорости дает параллельность тестов, а не микрооптимизации. Стабильность и явность поведения тестов важнее, время автоматизатора дороже времени выполнения автотеста.

  4. Чтобы управлять implicit_wait, webium’у требуется из любого места (из любого UI-объекта) иметь доступ к webdriver-объекту, и поэтому в webuim есть глобальный объект webium/driver.py at master · wgnet/webium · GitHub. Это плохо, но по-другому нельзя заимплементить, либо везде пробрасывать webdriver дополнительным объектом, что в коде выглядеть будет совсем некрасиво. Вот чтобы избежать этой глобализации в POM реализован собственный механизм ожидания: pom/base.py at master · schipiga/pom · GitHub. Кроме того благодаря этому, реализован также механизм ожидания отсутствия UI-элемента: pom/base.py at master · schipiga/pom · GitHub, что бывает весьма востребовано в тестах. При этом объект webdriver’a не достается, как туз из рукава, потому что про него знать не нужно. (P.S. Ошибаюсь, UI-Элементы в POM знают про webdriver, т.к. он нужен для action_chains: pom/base.py at master · schipiga/pom · GitHub, но только он извлекается не через глобальный объект)

  5. Webium использует implicit_wait, что приводит к появлению кода webium/base_page.py at master · wgnet/webium · GitHub. is_element_present - представлена не просто как самостоятельная функция, но ей даже уделена целая глава в документации, чтобы описать ее возможности и стратегии поведения с различными аргументами. Помоему высокоуровневый инструмент не должен заниматься тем, чтобы отдавать наружу порцию низкоуровневых операций. Если нужен low-level - нужно использовать webdriver напрямую. Код is_element_present не документирован, а разбираться в логике его стратегии мне как стороннему разработчику, честно совершенно не хочется. Такая избыточность кода с if, внутренними функциями и try-except’ами сразу наводит на мысль, что там что-то не так. Кроме того, implicit_wait ждет не тогда, когда элемент отобразиться, а когда появится в DOM’e, поэтому в webium появляется такой код webium/base_page.py at master · wgnet/webium · GitHub в попытке дать как можно больше гибкости. POM смотрит на проблему is_present по-другому: если элемент отображен - значит он есть, нет - значит нет (неважно в DOM’e он или нет) - также смотрит пользователь, а POM для user e2e-сценариев. И пока ни в одном тесте не возникло необходимости сделать по-другому.

  6. Webium декларирует UI-элементы, как проперти класса (не объекта!).
    Во-первых это заставляет делать Find дескриптором, что по-моему весьма странно для UI-элемента, знать что он должен быть дескриптором. UI-элемент должен знать только то, что связано с ним как с частью Page, а не то, что он должен быть дескриптором, потому что так задумали разработчики.
    Во-вторых, в методе _search_element webium/find.py at master · wgnet/webium · GitHub даже разбираться не хочется, какая логика там имплементирована, кажется что весьма сложная, лучше бы разработчики снабдили его подробной документацией - как никак ядро поиска. Вот это вот присвоение вообще выносит мозг webium/find.py at master · wgnet/webium · GitHub - подменять ссылку на класс - как минимум нужно подробно комментировать зачем, как максимум не делать никогда.
    В-третьих, судя по всему про потокобезопасный запуск тестов говорить не стоит в webium’e: если два параллельных теста из разных потоков будут работать с одним элементом одной страницы, при том что Find создает атрибут класса, а не объекта, скорее всего это приведет к сайд-эффектам. К примеру, насколько я знаю, при конкурентном запуске в webium/find.py at master · wgnet/webium · GitHub точно будет происходить перезапись объектов.
    P.S. или н-р код webium/find.py at master · wgnet/webium · GitHub видимо в качестве context’a может выступать только page, но об этом здесь ни слова.
    В отличие от webium, POM гарантирует потокобезопасность, поскольку для каждой инстанциированной страницы каждый UI-элемент будет уникальным благодаря коду pom/base.py at master · schipiga/pom · GitHub.

  7. В основном это все, можно еще прицепиться к тому, что в wait перехватывается WebDriverException, что весьма странно и не снабжено комментами. А также к тому, что webium не умеет работать со сложными структурами, типа таблиц, что в моем проекте весьма критично.

  8. В POM’е мне не нравится как пришлось реализовать кэширование: pom/base.py at master · schipiga/pom · GitHub. Но пока это некритично - нужно будет, буду думать как сделать проще или красивее. Кстати по поводу кэширования - его задача не столько обеспечить быстроту, сколько стабильность засчет перезагрузки кэша и повторного выполнения действий в случае, если элемент был перестроен в DOM’e в момент выполнения действий, что привело к исключению. Кэш дает прирост скорости в случаях, если нужно в течение некоторого времени опрашивать один и тот же UI-элемент, который должен измениться в результате асинхронных действий. Насколько прирост - не проверял, эта идея была заимствована из openstack/horizon, уверен без нужды бы его не стали делать.

  9. Также в POM’e мне не нравится, как написан модуль pom/fields.py at master · schipiga/pom · GitHub. Это потому что я заигрался с setter’ом и не учел проблем с наследованием. С будущем setter планируется заменить на обычную функцию.

  10. “Некоторые страницы имеют по несколько десятков полей. IDE не найдёт потом эти аттрибуты у класса.” Это проблема IDE, с т.з. питона все корректно. Н-р насколько я знаю, популярный pycharm до сих пор не может нормально осилить pytest. Не отказываться же теперь от pytest’a из-за этого, другие тестовые фреймворки рядом с ним нервно курят за углом.

  11. В целом я считаю, что код POM написан чище, понятнее, целостнее и проще, чем webium.

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

Классический implicit_wait нигде не использую. Фактически, есть webdriverwait(driver, timeout).until(condition(element)) который с Catberry, Angular, React работает отлично.Этот метод оборачивается удобными методами get_element() get_elements() (представим, что мы завели некий BaseSeleniumPage пейджобжект, в котором реализовали методы, которые применимы для любой вебстраницы) которые уже в рамках какого-то бизнес метода на определенной пейдже-потомка дергаются.

Храню ли я хэндлы на объекты страницы? Нет. Каждый раз происходит поиск по css или xpath - по мере вызова pageobject метода. А зачем мне хранить их? Зачем заводить кеши? Большую часть времени в тестах занимает серверный рендеринг, а не клиентский (т.е. всякие кейсы когда мы ищем не эл-т на странице, а его измененный атрибут - это мелочи по сравнению с тем, что классическая static html страница перегружается).

Т.е. тесты сейчас работают быстро, правильно с динамическими страницами. Может, дело в упрощении написания кода? Но для меня чем меньше импортов кода (а значит, и потенциально кода, который будет ломаться и за который ты не отвечаешь) - тем лучше.

@Evgenij_Buhgammer,

Никто не заставляет :slight_smile:

Перечислены весьма интересные вещи. Подкиньте пжста ссылку на код. Всегда интересно посмотреть код success_story-проекта с автотестами.

@Sergei_Chipiga спасибо за код-ревью. Причину написания собственных библиотек ради исправления фатального недостатка других я принимаю, хотя и не разделяю. :slight_smile:
Я не уговариваю использовать Webium, просто хотелось бы где-то уточнить те или иные выводы.

должно быть управление только через сущность application

Если хочется, то можно сгрузить все страницы в одно место. Webium в этом никак не ограничивает.

Implicit_wait, который использует webium

Он отключается одной настройкой, если он не нужен.

гарантирует потокобезопасность

Это сомнительная фича для python’a ввиду наличия GIL. Тесты параллелятся на уровне процессов, а не потоков, как в Java.

webium не умеет работать со сложными структурами, типа таблиц, что в моем проекте весьма критично

Webium создавался для работы со сложными структурами. Это прямой fork идей HtmlElements на python.

Это проблема IDE, с т.з. питона все корректно.

Если IDE не распознаёт мой код, то страдаю я, а не IDE. И к сожалению, это моя проблема, а не IDE.

1 лайк

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

Это всё хорошо знать по поводу GIL, a тестам это зачем? pytest, nose распараллеливают за счёт процессов.

Код автотестов скидывать я тут не буду (NDA, все дела), но могу более подробно описать, как все разруливалось. Вопрос в том, что конкретно из откровений ожидаете увидеть: это не Selenide, Это просто чистый селениум и обертки над ним, который решает свою задачу. Сайт для примера: flamp.ru

Про GIL тестам знать незачем. Питон поддерживает многопоточность (плохо или хорошо - другой вопрос), webium написан так, что код не потокобезопасный. От того, что тестовые фреймворки делают параллельность не через многопоточность, а через процессы - это не делает код webium’a лучше или надежнее. К примеру, если я захочу использовать webium как средство автоматизации, для каких-либо своих задач, где будет многопоточность, то работать он не будет правильно. По-моему либа не должен полагаться на то, что не реализовано сейчас в других инструментах ее использующих. Она вообще про них ничего не должна знать. Конкретно для webium’a на мой взгляд правильно писать в документации, что не предназначен для многопоточного выполнения кода.

P.S. Кстати проблемы могут возникнуть даже в однопоточном режиме, когда один писатель изменит дескриптор, потом второй писатель изменит этот же дескриптор, потом первый попытается опять провести действие с этим дескриптором, и жестко обломается. Такое выглядит маловероятным, но чем маловероятнее ситуация, тем сложнее отловить будет баг.