Удаленка для jenkins+selenide+selenoid+allure+docker спецов на 2-3 часа в день. 100% remote! Присоединиться к проекту

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

design-patterns
selenium
framework
python
page-object
architecture
webdriver
Теги: #<Tag:0x00007fedc7a9ae68> #<Tag:0x00007fedc7a9ad28> #<Tag:0x00007fedc7a9abc0> #<Tag:0x00007fedc7a9aa80> #<Tag:0x00007fedc7a9a918> #<Tag:0x00007fedc7a9a7d8> #<Tag:0x00007fedc7a9a648>

(Sergei Chipiga) #1

Всем привет,

Как и обещал, выкладываю статью по архитектуре и концепции POM (http://pom.readthedocs.io/, https://github.com/sergeychipiga/pom).

Почему нужен враппер над selenium

POM - это микрофреймворк, имплементирующий page object model для тестирования webUI-приложений через selenium. POM берет на себя обязанности по низкоуровневым манипуляциям с DOM-элементами через selenium.

Часто случается видеть в коде (особенно начинающих специалистов в автоматизации) методов page зашитые константы селекторов, слипы и прочие признаки плохо “пахнущего” кода. С одной стороны - вроде бы все по феншую: page-object-pattern, скрытая логика реализации. Но на деле имеем повторяющийся плохочитаемый плохоструктурированный код.

Важно соблюдать правило: “логика реализации должна быть отделена от логики использования”. Очень хочется работать со структурой page в объектном стиле, о чем и говорит page object pattern. Очень хочется обращаться к UI-элементам как к объектам, вызывать у них методы и следовать иерархии вложения элементов. Вот это и делает POM.

Использование POM позволяет декларативно описать структуру и иерархию страницы один раз (при условии что верстка, а соответственно селекторы и структура, не меняются) (н-р: https://github.com/Mirantis/mos-horizon/blob/v9.1/horizon_autotests/app/pages/containers.py), и затем уже у себя в тестах или стэпах обращаться к UI-элементам так, как того требует тест или стэп (https://github.com/Mirantis/mos-horizon/blob/v9.1/horizon_autotests/steps/containers.py).

Какие типы элементов выделяет POM

POM делит все UI-элементы на две большие категории:

  • контейнеры (страницы, блоки, таблицы, списки), которые могут содержат внутри себя простые элементы. При этом у контейнеров также есть все методы типа click() и т.п., которые доступны простым элементам. (https://github.com/sergeychipiga/pom/blob/master/pom/ui/table.py#L143)

Page также является контейнером, что в общем-то вполне очевидно. Технически, контейнер является mixin-классом (https://github.com/sergeychipiga/pom/blob/master/pom/ui/base.py#L53), который подмешивается к базовому UI-классу (https://github.com/sergeychipiga/pom/blob/master/pom/ui/base.py#L131).

Как компонуется иерархия UI-элементов

Контейнеры регистрируют (https://github.com/sergeychipiga/pom/blob/master/pom/ui/base.py#L57) внутри себя другие UI-элементы (простые и контейнеры), создавая иерархию. При этом механизм регистрации работает следующим образом:

  • При непосредственно регистрации ui, создается шаблон UI-элемента с селектором.
  • К классу добавляется property, связанный с созданным шаблоном
  • При вызове property у инстанциированного контейнера создается клон из шаблона, которому указывается родительский элемент (контейнер)
  • Клон кэшируется и при последующих вызовах уже берется из кэша, а не клонируется заново.

Таким образом все инстансы одного контейнера будут иметь разные клоны с одного зарегистрированного UI-шаблона. Это важно н-р при многопоточном запуске тестов, т.к. позволяет писать by design потокобезопасные тесты.

Как реализуется ожидание UI-элементов

Большинство методов UI-элементов (https://github.com/sergeychipiga/pom/blob/master/pom/ui/base.py#L131) имеют декоратор @wait_for_presence. Возникает вопрос - зачем, если уже selenium внутри себя реализует implicit_wait, который умеет дожидаться элемент в DOM’e. На это есть несколько архитектурных обоснований:

  • seleinum - это библиотека низкоуровневой работы с DOM и в ней implicit_wait используется, чтобы дождаться появления элемента именно в DOM’e, даже если он будет при этом невидим (а следовательно некликабелен). Так что если элемент есть в DOM’e, но еще не стал видимый, то implicit_wait вас не спасет и нужно изобретать хаки. POM по-другому смотреть на присутствие UI-элемента: с точки зрения пользователя, UI-элемент присутствует, если он видим. И кастомный waiter позволяет легко этого добиться (пусть и ценой постоянных запросов к driver’у).
  • implicit_wait потому так называется, что он неявный, и происходит неявно, даже тогда, когда вам этого не нужно, тратя на это драгоценное время теста. Н-р: если нужно сразу сказать: присутствует элемент или нет, implicit_wait этого вам не позволит, попытавшись сперва дождаться элемента. Обычно для обхода этого используется хак (н-р в виде контекст-менеджера): сперва implicit_wait устанавливается на 0, затем запрашивается статус присуствия элемента, потом восстанавливается прежнее значение implicit_wait. Но implicit_wait - это метод driver’a, а элемент POM’a знает только про selenium-элемент с которым связан и ничего не знает про driver (responsibility segregation). Протаскивать вглубь POM-элементов знания о driver - это был бы хак.
  • POM использует механизм кэширования selenium-элемента, чтобы не обращаться без необходимости к driver’у. При этом если кэш тухнет, нужно быстро перезапросить selenium-элемент, изменив значение implicit_wait на 0, что возвращает к предыдущему пункту.
  • отказ от implicit_wait позволило единообразно организовать у UI-элемента методы wait_for_presence, wait_for_absence, т.к. в тестах довольно часто бывает нужно убедиться, что после действий элемент появился или исчез во времени.

Как работает механизм кэширования UI-элементов

POM выстраивает иерархию UI-элементов, при этом каждый UI-элемент связан с selenium-элементом, через которое производится действие в браузере. Selenium обеспечивает методы поиска внутри элемента find_element и find_elements, что позволяет проводить иерархичный поиск - это же и является сердцем POM’a. Когда в цепочке производится действие н-р: page.block_1.block_2.block_3.button.click(), при вызове метода click, элемент button ищет внутри родителя cвязанный selenium-элемент, поскольку родитель block_3 пуст - он начинает искать свой связанный selenium-элемент внутри своего родителя block_2 и так далее. При первичном запросе на каждом уровне иерархии производится выборка selenium-элемента с последующим его кэшированием. При последующем вызове метода у UI-элемент, он достается сперва из кэша, и если он устарел (PRESENCE_ERRORS = (exceptions.StaleElementReferenceException, exceptions.NoSuchElementException)), то производится перевыборка с обращением к родителям.

Стоит отметить что POM не использует find_elements, хотя имеет поддержку. Это связано с тем, что POM работает с каждым объектом индивидуально, храня его в кэше. На больших списках или таблицах данных это может приводить к длительному времени выполнения (хотя думаю даже на голом selenium с find_elements время опроса свойств каждого элемента станет существенным, тут уж проще использовать вызов js и выполнять калькуляцию на стороне браузера), однако selenium - это впервую очередь инструмент функционального тестирования (функциональность можно проверить и на списке с 10 элементами), и никак не инструмент нагрузочного тестирования.

На этом все, наверняка что-то упустил, поэтому готов ответить на вопросы.

import unittest

import pom
from pom import ui
from selenium.webdriver.common.by import By


@ui.register_ui(field_login=ui.TextField(By.NAME, 'email'),
                field_password=ui.TextField(By.NAME, 'pass'))
class FormLogin(ui.Form):
    """Form to login."""


@ui.register_ui(form_login=FormLogin(By.ID, 'login_form'))
class PageMain(pom.Page):
    """Main page."""
    url = '/'


@ui.register_ui(
    alert_message=ui.Block(By.CSS_SELECTOR, 'div.uiContextualLayerPositioner'))
class PageLogin(pom.Page):
    """Login page."""
    url = '/login'


@pom.register_pages([PageMain, PageLogin])
class Facebook(pom.App):
    """Facebook web application."""
    def __init__(self):
        super(Facebook, self).__init__('https://www.facebook.com', 'firefox')
        self.webdriver.maximize_window()
        self.webdriver.set_page_load_timeout(30)


class TestCase(unittest.TestCase):

    def setUp(self):
        self.fb = Facebook()
        self.addCleanup(self.fb.quit)

    def test_facebook_invalid_login(self):
        """User with invalid credentials can't login to facebook."""
        self.fb.page_main.open()
        with self.fb.page_main.form_login as form:
            form.field_login.value = 'admin'
            form.field_password.value = 'admin'
            form.submit()
        assert self.fb.current_page == self.fb.page_login
        assert self.fb.page_login.alert_message.is_present

STEPS-архитектура на примере horizon-тестов openstack'a.
(Pavel Ponomaryov) #2

Действительно ли даёт выигрыш по времени обращение к кешированному элементу? И часто ли нужно это делать в принципе? Как правило мы элемент либо кликаем, либо получаем/вводим текст единожды, затем производим какие-то проверки.

Вообще, нечто похожее я делаю на #java с #selenide . Только вдобавок ещё использую и #cucumber - который по мне так более читабельный, чем ваша простыня. Надо будет тоже как-нибудь выложить пример.


(Mykhailo Poliarush) #3

Выложите вашу реализацию в #baza-znanij, мы и посмотрим на хороший пример!


(Sergei Chipiga) #4

Болтовня ничего не стоит. Покажите мне код.
— Linus Torvalds

Пожалуйста, ваш код в студию, будем обсуждать.


(Sergei Chipiga) #5

В 100% случаев выигрыша безусловно не будет. Однако живой пример с пользой кэша из хорайзона:

Опенстек позволяет выполнять асинхронные операции, н-р забутать nova-сервер. При этом в UI хорайзона это выглядит как строка в таблице, внутри которой имеется лейбл, меняющий свое значение с “Build” на “Active” после того как nova-server загрузится и перейдет в режим ожидания. Это время порядка до 3 минут (везде по-разному, зависит от мощности железа под тесты), в течение которых нужно опрашивать значение лейбла. Кажется закэшировать строку в таблице, внутри которой содержится лейбл (меняющийся в DOM-дереве), выглядит неплохим подходом.

В целом кэширование помогает не столько в производительности, сколько в надежности, когда можно наткнуться на момент, что элемент находился в недоступном состоянии (особенно в динамически меняющихся одностраничных приложениях), и получить flaky-тест


(Pavel Ponomaryov) #6

Ну вот, надо было мне лезть в тему :smiley: Придётся публиковать


(Sergei Chipiga) #7

@pavelp, да пожалуйста. Очень хочется увидеть как с помощью кукумбера избавиться от простыней :wink: Плюс хотелось бы на базе вашего решения поспрашивать, как написать сложные e2e сценарии с количеством шагов около 25-30, которые сейчас в тесте имплементируются через 15-20 вызовов переиспользуемого кода и вовлекают в тест более 10 сущностей. Пример можно посмотреть в https://github.com/Mirantis/stepler/blob/master/stepler/nova/tests/test_deferred_delete.py#L28.

При текущем решении стэпы обмениваются сущностями как объектами, гарантируют результат teardown’a для каждого ресурса независимо. Интересно как это хотя бы на уровне прототипа будет выглядить в вашем варианте через given-when-then-and. Насколько я знаю, кукумбер не оперирует объектами, лишь примитивными типами данных.


(Pavel Ponomaryov) #8

Не буду пока делать отдельный пост ну вот к примеру огурцовый (cucumber) feature файл

@web
Feature: Login
  Scenario: Check login form placeholder texts
    Given user is on login page
    Then field "EMAIL" should have placeholder text "Type your email"
    And field "PASSWORD" should have placeholder text "Type your password"

Элементарный пример, само собой. Кто не в курсе как работает огурец (https://cucumber.io/docs/reference/jvm#java) - реализация каждого шага спрятана в отдельных классах - так называемых Step Definitions. Для шагов на странице Login у меня есть класс LoginPageSteps.java. Пример реализации шагов

public class LoginPageSteps extends XHubSteps {

    @Inject
    LoginPage loginPage;

    @Given("^user is on login page$")
    public void userIsOnLoginPage() throws Throwable {
        loginPage.init(); // login url is opened before scenarios with @web tag
    }

    @Then("^field \"([^\"]*)\" should have placeholder text \"([^\"]*)\"$")
    public void fieldShouldHavePlaceholderText(String inputLabel, String text) throws Throwable {
        inputByLabelText(inputLabel).shouldHave(attribute("placeholder", text));
    }
}

Некоторые пугаются регулярных выражений в аннотациях, но честно говоря они генерируются плагином сами на лету с помощью горячих клавиш. Нам остаётся лишь инициализировать объект loginPage и получить доступ к необходимым методам или элементам. Метод init в базовом классе ждёт загрузки страницы (в моём случае пока не исчезнет визуальный progress bar или не произойдёт таймаут). Так я гарантирую загрузку страницы и готовность её к работе. Для повышения стабильности тестов init() необходимо вызывать каждый раз, когда происходит перезагрузка страницы. В самих page objects init() так же проверяет видимость ключевых элементов на странице (да, знаю, сейчас начнут закидывать помидорами, но я считаю это правильно), по которым можно понять, что загрузка страницы действительно удалась.

Селенидовский $() возвращает элемент с которым я могу уже осуществлять некие манипуляции. Для работы с коллекцией элементов используется $$(). Далее - shouldHave() (и аналогичные - should(), shouldBe()) - это мощный селенидовский механизм проверки соответствия условиям, коих великое множество. Всё готово к использованию out of the box, как говорится, но можно дописывать и свои условия или Conditions, если проверка какая-то хитрая. Например shouldBe(Condition.visible) - это самая простая проверка, заменяющая assert но при этом с ожиданием. Если она упадёт Selenide ещё и скриншот сделает. Ну тут уже на любителя - кто как репортит ошибки.

@ScenarioScoped
public class LoginPage extends AuthenticationPage {

    public final By email = By.name("email");
    public final By password = By.name("password");
    private final By loginButton = By.xpath("//input[@value='Log in']");
    private By forgotPasswordLink = By.linkText("Forgot your password?");

    public LoginPage() {
    }

    public LoginPage init() {
        super.init();
        return this;
    }

    public LoginPage setEmail(String login) {
        $(email).click();
        $(email).clear();
        $(email).sendKeys(login);
        return this;
    }

    public LoginPage setPassword(String pass) {
        $(password).click();
        $(password).clear();
        $(password).sendKeys(pass);
        return this;
    }

    public XHubPage loginAndVerify() {
        $(loginButton).click();
        $(errorMessage).shouldNotBe(visible);
        return page(XHubPage.class).init();
    }

    public void clickLogin() {
        $(loginButton).click();
    }

    public ForgotPasswordPage openForgotPasswordPage() {
        $(forgotPasswordLink).click();
        return page(ForgotPasswordPage.class).init();
    }
}

Селенид сам осуществляет проверку доступности элемента по таймауту, который определяется заранее через Configuration.timeout. Если мы пишем что-то вроде $(“input”).click(), Selenide сперва будет ждать видимости элемента, лишь потом кликать, что избавляет нас от необходимости писать свои ожидания (можно конечно и свои запилить, если есть большое желание).

Благодаря простоте такой архитектуры, написание тестов превращается в сплошное удовольствие и читать их очень легко, особенно огурцовые спеки. Если кому интересно узнать больше, то вынесу в отдельный пост более подробное описание и может некий скелет проекта на github закину. Хотя, сдаётся мне я Америку не открыл и каждый второй пишет примерно в таком стиле.

PS. @Inject и @ScenarioScoped аннотации пока можно игнорировать, ибо они стоят отдельной темы. :smiley:


(Pavel Ponomaryov) #9

Да, это определённо стоит отдельной темы. Все ваши вопросы решаемы, но я пожалуй не буду засорять вашу тему


(Sergei Chipiga) #10

@pavelp, чуть позже отпишу даже на базе этого простого примера почему так точно никогда не нужно делать, особенно для тестирования сложных и интерпрайзных продуктов :slight_smile:

Однако первый вопрос вот в чем, если мне в каждом тесте нужно проводить авторизацию, при такой методике мне нужно в каждый тест делать копипасту из Given-Then-And - описывающей примитивные действия?

И кроме того, у нас стэпы содержат внутри себя верификацию что стэп совершил верное действие (верификация опционально отключаема для негативных тестов). В случае кукумбера я так понимаю When и Then (верификация) должны быть отдельно и должны полностью копироваться в все тесты, требующие этого действия?


(Sergei Chipiga) #11

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


(Pavel Ponomaryov) #12

хехе, прям уж так никогда. :smiley: Зачем же делать копи пасту, если есть шаги более высокого уровня - Например Given project page is open - которая уже содержит в себе все промежуточные шаги. Нет верификация зашита в действие, которое вы совершаете. Просто реализация степов разная. Как в моём примере, есть шаг loginAndVerify а есть просто clickLogin после которого требуется отдельная верификация. Или я вас неправильно понял?


(Pavel Ponomaryov) #13

Cucumber рекомендует использовать Injection - это, то чем я и пользуюсь. Сущности - то бишь объекты создаются раз за сценарий, а потом мы получаем к ним доступ в любом Step Definition классе.


(Sergei Chipiga) #14

@pavelp, поскольку вы выложили код (хотя и очень примитивный) позволю себе рассказать, почему на мой взгляд так не стоит делать по следующим пунктам:

  • cucumber, синтаксис и имплементация gherkin;
  • структура класса steps;
  • структура класса page;

Недочеты в cucumber и gherkin:

  • Это текстовый документ со строковыми значениями.
  • Вряд ли под него имеется достойный code completion.
  • В строковых значениях можно легко опечататься.
  • Такой тест не оперирует объектами, а лишь текстовыми параметрами. Объекты нужно извлекать руками внутри методов, либо хранить их в каком-то скрытом буфере (как было сказано в комменте выше), что весьма неявно.
  • Честно с трудом представляю как дебажится такой тест, когда нужно встать перед каким-то стэпом и последовательно из консоли выполнить ряд последующих шагов, чтобы отследить ошибку.
  • Это красиво выглядит в глазах менеджемента: “Смотри, просто пишем текст теста и оно само работает.” Только вот внутри “оно само” порой часто скрывается весьма сложная логика. Тесты должны быть организованы как можно проще, без лишних абстракций. Автотесты пишутся автоматизаторами на языке программирования.

Недочеты класса steps:

  • Первое что бросается в глаза, это ужасные регулярки. Да сказано, что они сами генерятся и т.п. и вроде даже работают. Но вряд ли сходу можно понять, что таком происходит, а если вдруг ее понадобиться изменить и не ошибиться - наверное это сложно. В общем читается это однозначно тяжело.
  • Ну и конечно ЯОченьЛюблюЧитатьДжаваКодОнТакойПонятный, потому что это гораздо лучше чем код_который_написан_через_подчеркивания.
  • В остальном что-то сказать больше трудно, т.к. кода там фактически нет. Хотя хотелось бы увидеть примеры с использованием упомянутого буфера сущностей и какие-либо более сложные стэпы, чтобы составить впечатление.

Недочеты класса pages:

  • Самая большая архитектурная ошибка, которую часто приходится видеть - это делать у page методы для управления элементами. Это фундаментально неверно: page - это контейнер, который ничего не умеет, умеют что-то элементы, которые внутри него находятся. Самое правильное - это обеспечить иерархичное обращение к элементам и к их методам. Это гибко, т.к. позволяет обращаться к любому задекларированному элементу, когда это нужно, и не плодить кучу методов типа set_email, set_password, click_login, clock_recovery_button и т.п. Они не нужны, когда можно сделать page.button_login.click() - и это будет очень ООП.
  • Приведенные методы очень простые, и похоже обращаются напрямую к selenium-элементам, однако хотелось бы увидеть как в таком варианте работают с таблицами, строками и ячейками, и другими сложными структурами. Кажется, что пахнет чудесами в коде.
  • Оставляет негативное впечатление дублирование кода вида: $(password).clear(); $(password).sendKeys(pass);. Хотя вроде можно обеспечить высокоуровневый метод ui_element.set_text('some text').
  • По поводу методов управления UI элементами в классе page’ы. C одной стороны это выглядит вроде здорово, что можно отнаследоваться, переопределить селекторы, методы - и при этом в стэпе ничего не сломается. Но на деле получается в таком случае, что класс стэпов в целом не нужен, поскольку вся логика оттуда вынесена в page, либо разделена между двумя классами, что тоже неправильно. Такой подход нарушает первый принцип SOLID - single responsibility, согласно которому в частности page - отвечает за структуру и иерархию ui-элементов, steps - за действия над ui-элементами страницы. Не стоит создавать GodObject, умеющий все.

Честно говоря, позволю себе не поверить на слово, что “благодаря простоте такой архитектуры, написание тестов превращается в сплошное удовольствие” (такое понятие вообще редко в тестировании). Среди того что даже на этом примере видно - это перегруженность абстракциями (засчет cucumber’a и его костылей), сложность дебага, возросшая цикломатическая сложность кода, некорректное построение архитектуры. Возможно это будет удовольствием на первых порах, но есть сомнения что такие тесты хорошо масштабируются и переносятся на разные версии продуктов и при этом легко сапортятся. Однако опять же, уважаемый @pavelp, будет здорово если вы скинете ссылку на реальные тесты и реальный продукт который тестируете, чтобы оценить.


(Pavel Ponomaryov) #15

Я честно говоря не особо старался, приводя этот примитивный пример, поэтому многие пункты из вашего комментария, при всём уважении не относятся к делу. Вы делаете умозрительные заключения о сложности поддержки тестов, не имея полного представления о том как работает cucumber или его интеграция с IDE. (Не скажу, что я сам мега-специалист, но тем не менее).

Сложных тестовых сценариев у нас более чем достаточно, и а сам продукт вы можете пощупать тут http://hub.xrebel.com, зарегистрировавшись через https://zeroturnaround.com/software/xrebel-hub/ У нас сейчас как раз идёт период публичной беты, так что рады будем любым отзывам.

Но давайте по пунктам

  • cucumber, синтаксис и имплементация gherkin;

Это текстовый документ, который делает ваши тесты читабельными - как вами, так и вашей командой. На выходе вы получаете отчёт, который человеческим языком говорит, что конкретно пошло не так.

Достойный code completion есть - без него вообще не было бы смысла заморачиваться, иначе бы поддержка тестов превратилась в ад и садомию.

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

То, что вы называете тестом, на самом деле является исполняемой спецификацией. То есть feature файл есть спецификация или как их называют executable specification. Объектами в спецификациях никто не оперирует - это не их задача. Я понимаю, что это сложно принять - поначалу. Я сам изначально был весьма скептично настроен, пока не написал несколько тестов и не оценил по достоинству. Меня тоже изначально волновал вопрос переиспользования созданных объектов и это было моим главным препятствием для перехода от JUnit тестов к Cucumber. Dependency Injection, как оказалось - это весьма стандартная практика и ничего неочевидного в ней нет. Почитать о том как она работает поможет google. В наших тестах используются весьма сложные сценарии с поднятием докер контейнеров, деплоем тестовых приложений и прочими хитростями - всё это реализуется через Dependency Injection и после написания одного такого сценария становится интуитивно понятным.

“Честно с трудом представляю как дебажится такой тест…” Дебажится элементарно, стандартными средствами IDE. Более того - из-за того что шаги стандартные и переиспользуемые определить где и что идёт не так очень просто.

Менеджмент меня не особо волнует - читать тесты оно не будет - у них на это нет времени. И это вообще не их забота.

  • структура класса steps;
    Серёж, серьёзно - ужасные регулярки? Там два - три варианта от силы и те генерируются сами. Тебе не нужно на них вообще смотреть. Можно закрыть листочком эту строчку и забыть про неё во время написания шага - ОК? Там структуры нет - это просто коллекция steps - эдакий склад, куда ты положил степ и забыл. ключевое слово у него может быть любым - given, when, then между ними нет никакой разницы. Один и тот же шаг ты сможешь использовать в спеке с любым ключевым словом. (Given I am logged in = When I am logged in … etc)

По поводу “Ну и конечно ЯОченьЛюблюЧитатьДжаваКодОнТакойПонятный…” я вообще ничего не имею против python, сам с него начинал, но не прижился он у меня. Читаем то мы в итоге не java code а feature.

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

  • структура класса page;

“Самая большая архитектурная ошибка, которую часто приходится видеть - это делать у page методы для управления элементами.” Тут я с вами частично соглашусь. Исторически эти методы были реализованы так и в этом нет ничего страшного. Это простой класс - так что вообще не критично. Во многих моих page objects реализованы методы возвращающие элемент для дальнейших манипуляций, как вы и пишете. Для элементарных методов с простыми методами типа click() или sendKeys ваш комментарий вполне имеет смысл, но цель методов зашитых в page object в том, что они делают, всё, чтобы выполнить определённое действие. Если я хочу выбрать дату в datepicker я не буду извне page object предоставлять доступ к элементу datepicker и потом с ним устраивать пляски с бубном. Я лишь вызову page.setPeriod(start, end). Это лишь один пример, а таких может быть множество. Мой метод выбора даты будет работать в JUnit тесте и в шаге Step Definition для Cucumber - таким образом могут параллельно существовать два фреймворка и я не буду писать реализацию выбора периода для каждого из них.

Насчёт таблиц и прочих сложных компонентов - никаких чудес. Selenide предоставляет удобные методы для получения данных - не нужно самому изобретать велосипедов. http://selenide.org/documentation.html тут есть некоторые примеры

“Оставляет негативное впечатление дублирование кода вида: $(password).clear();” Ну вы уже начинаете придираться и делать code review. Я не делал целью написать идеальный код. Да, есть у Selenide другой метод - element.setValue(text), которые делает всё это сам. Тут уже мы отходим от темы…

Насчёт того зачем вообще классы stepов - этот слой нужен для работы cucumber. Честно говоря не вижу проблемы, так как steps могут быть любой сложности и более того могут в себе содержать другие steps. page object отвечает за элементарные операции на странице а steps фактически являются вашими тестами. Каждый шаг - это своебразный микро тест, который упав, уронит весь сценарий. В общем - я бы на вашем месте отбросил предвзятость и просто попробовал пощупать cucumber своими руками, чтобы составить полноценное представление о его плюсах и минусах. В любом случае я придерживаюсь своего мнения, что читать мой примитивный текстовый файл будет проще, чем, то что привели в примере вы.


(Sergei Chipiga) #16

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

По cucumber’у я уяснил следующее, что идет сильная привязка к IDE. Хотя вроде в мире Java без сильной IDE делать, как я слышал, нечего. Окей, не очень приятно становится заложником IDE, но ок, видимо это удобно, если привыкнуть.

Однако мне интересно, если стэп меняется - то есть меняется его строковое описание, насколько сложно будет поменять это во всех тестах, использующих этот стэп? Будет ли это ручная операция или IDE все пофиксит без проблем?

Регулярки действительно смотрятся ужасно, особенно если знаешь, что при другом подходе их не будет. Но видимо так cucumber работает.

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

“Если я хочу выбрать дату в datepicker я не буду извне page object предоставлять доступ к элементу datepicker и потом с ним устраивать пляски с бубном. Я лишь вызову page.setPeriod(start, end).” - вот это вот в корне неверно. Опять повторюсь, что страница - лишь контейнер. Этот datepick-объект должен предоставлять высокоуровневый API setPeriod, чтобы установить дату, без всяких плясок. И это будет гибко, поскольку не придется в каждой странице, использующей datepick-объект имплементить метод setPeriod (конечно можно сделать его один раз в базовом классе, но не все наследники могут содержать datepick-объект. Можно сделать миксин и подмешивать через множественное наследование и тогда страница будет компоноваться из методов, подмешанных от множества миксинов в конечном итоге).

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

“В общем - я бы на вашем месте отбросил предвзятость и просто попробовал пощупать cucumber своими руками, чтобы составить полноценное представление о его плюсах и минусах.” - Сожалею, что создалось впечатление о предвзятости, но оно основывается на вашем примере. Вы утверждали, что сложные тестовые кейсы можно описать очень легко через cucumber, к сожалению пример “hello world” не раскрыл всей прелести и не показал, как это выглядит на тяжеловесных сценариях.

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


(Pavel Ponomaryov) #17

В декабре, кстати, буду в Москве на конференции Heisenbug - можно было бы пересечься, поболтать.

В отношении IDE - да, писать на Java без IDE как-то сложновато. Можно конечно, но неудобно.

По поводу рефакторинга строки шага - примечательно то, что в настоящий момент в плагине IntelliJ для огурца есть баг, в результате которого не переименовывается шаг. То есть поддержка есть, но не работает. Я репортил , но что-то не видать изменений. Это напрягает, но, думаю, будет в итоге исправлено. Сам я пока что решаю этот вопрос простым find and replace. В общем-то это уже детали. Можно ещё в step definitions классе ткнуть комбинацию клавиш на методе и увидеть в каких feature файлах он используется.

“опыт тестирования показывает, что оперировать объектами в тесте очень даже нужно” - ещё раз. feature файл это спецификация, а не сам тест. То бишь документация. Исполняемая. Это можно сказать более высокий уровень над тестом. Сам же тест в реализации шагов - там и используются объекты.

datepicker - да, вы правы. Так он и реализован. Но вот влепить прокси метод в page object мне ничего не стоит. Это дёшево. Тогда в тесте будет не page.datepicker.setPeriod(bla,bla) а на одно промежуточное обращение меньше.

А если честно, то я вообще не программист. :smiley: поэтому мне сложно спорить о высоких материях ))


(Sergei Chipiga) #18

@pavelp, к сожалению пересечься в Москве не получится. Сам нахожусь не в России, и пока туда не собираюсь еще долго. Но буду рад пообщаться в переписке. А вообще спасибо за тему про cucumber - уже с интересом читаю статьи :slight_smile:


(Roma Marinsky) #19

а зачем этот метод? Что вы там инициализируете особенного


(Roma Marinsky) #20

Очень не практично дублировать такой код, постому что для разных тестов придётся обращаться к элемнету и от него вызывать метод для манипуляции. Ну к примеру если кастомный элемент, так что, завуалирую, простым кликом не обойтись. А особенно это касается установки значения в элемент или селект в выпадайке.[quote=“Sergei_Chipiga, post:14, topic:11777”]
Оставляет негативное впечатление дублирование кода вида: $(password).clear(); $(password).sendKeys(pass);. Хотя вроде можно обеспечить высокоуровневый метод ui_element.set_text(‘some text’).
[/quote]

у selenide есть setValue(), который очистит и отправит символы

Я бы предпочёл не использовать строго принципы SOLID - это усложняет автоматизацию и повышает порог вхождения в проект с тестами, т.к. плодиться может слишком много объектов. Естественно, это не касается очень сложных legacy проектов, где уж действительно лучше писать тесты на основе solid. А на современных проектах лучше вообще забыть про солид, в рамках разумного конечно.
Чем проще вы построите архитектуру проекта с вашими тестами тем проще будет это переписать при переходе на новый фреймворк вашего сайта или совсем при ребилде. Ну и код будет просто выглядеть, вы сможете “ручников” быстрее адаптировать под автотесты, чтобы они смогли их сами писать, а вы занимались бы ревью и архитектурой

Вот это вообще ни к месту, честно говоря, цикломатическая сложность это про вложеность условий/циклов, а не сценариев ui теста

А вот это в принципе касается уже BDD подхода))) Который даёт ложную видимость простоты. Да и в целом, редко кто применяет его так как его задумали, это действительно рабочий подход, но только если знать и уметь им пользоваться правильно[quote=“pavelp, post:15, topic:11777”]
Это текстовый документ, который делает ваши тесты читабельными - как вами, так и вашей командой. На выходе вы получаете отчёт, который человеческим языком говорит, что конкретно пошло не так.
[/quote]

При xunit подходе тоже не трудно сделать очень читабельный отчёт об упавшем тесте. А читабельность на уровне человеческого сценария лишает гибкости проверки того что можно дополнительно проверять. Баги могут просачиваться между шагами сценария[quote=“pavelp, post:15, topic:11777”]
Я сам изначально был весьма скептично настроен, пока не написал несколько тестов и не оценил по достоинству. Меня тоже изначально волновал вопрос переиспользования созданных объектов и это было моим главным препятствием для перехода от JUnit тестов к Cucumber.
[/quote]

только если грамотные специалисты пишут геркин сценарии можно добиться профита от BDD, надеюсь вам повезло с этим)[quote=“pavelp, post:15, topic:11777”]
Менеджмент меня не особо волнует - читать тесты оно не будет - у них на это нет времени. И это вообще не их забота.
[/quote]

а вот это плохо… у вас не используется по назначению BDD… а просто так - это ж клёвый подход, читабельные тесты рулят. С инженерной точки зрения, качество проверки важнее читабельности сценария. Тут уже важно какие логи вы прикладываете к отчёту о баге