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

Я привёл пример - тест не на открытие, а на закрытие. При выборе значения дропдаун может и закроется, а вот при клике вне дропдауна нет. Это разные сценарии. Возможно вы и правы насчёт порога вхождения - сложно судить, так как это вещь субъективная. В любом случае придётся вникать откуда что приходит и какие методы доступны через объект. А уж нажать шорткат в 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. Кстати проблемы могут возникнуть даже в однопоточном режиме, когда один писатель изменит дескриптор, потом второй писатель изменит этот же дескриптор, потом первый попытается опять провести действие с этим дескриптором, и жестко обломается. Такое выглядит маловероятным, но чем маловероятнее ситуация, тем сложнее отловить будет баг.

На самом деле всегда интереснее смотреть код, чем читать описание. Но в ок, раз нельзя код, давайте описание :slight_smile:

Я в Selenide не разбираюсь :slight_smile: Из откровений - все что вы считаете нужным.

если я захочу использовать webium как средство автоматизации, для каких-либо своих задач, где будет многопоточность, то работать он не будет правильно

Если я захочу использовать молоток в качестве отвёртки, то он будет работать неправильно - более чем согласен. Инструмент делался для тех задач, для которых он был нужен.

один писатель изменит дескриптор, потом второй писатель изменит этот же дескриптор

Не понимаю, о чём речь. Писатель - это человек, пишущий код? В процессе выполнения кода дескрипторы никто не меняет.

По-моему некорректное сравнение. Webium позиционируется как инструмент для автоматизации UI-действий через selenium. Я хочу его использовать для автоматизации рутины с UI, и хочу это выполнить в несколько потоков, в каждом из которых будет запущен браузер и на одной и тоже же странице проведены некоторые действия с UI-элементами, н-р в 2 потока заполняется и отправляется форма, и проч. Webium получается не подходит, несмотря на все свои вкусности. Нужно искать или изобретать что-то другое.

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

Писатель - это объект, изменяющий (записывающий новые свойства) в другой объект. Когда в коде запрашивается объект-дескриптор, производится изменение его свойства self.context = obj. В принципе кажется, что в однопоточном режиме 'это присвоение не приводит к потенциальным сайд-эффектам, потому что с self.context вроде никаких апдейтов не делается в других местах. И думаю, что и в многопоточном режиме их не должно быть (опять же кто знает. P.S. все таки кажется, что параллельно не будет надежности, т.к. вызов _search_element может занять время, а в это же время self.context будет перезаписан в другом потоке), но мне кажется, что выглядит это очень неявно за счет return self._target_element и того как и где оно вычисляется.