Pull to refresh

Comments 7

Конечно, можно сказать, что любой сценарий состоит из последовательности простых действий. Может, в идеальном мире, так и было бы, но если попробовать написать тест для CRM, содержащей десятки таблиц, множество виджетов в каждой записи, сложные фильтрации, поисковые запросы, загрузки, импорты данных, динамические события и прочее – итоговый тест может состоять из 50–60 строк, где каждая строка является достаточно сложным шагом. После этого, взглянув на автогенерированный отчёт, спорить о его "легкочитаемости" будет крайне сложно.

Для начала стоит вспомнить, что перед тем, как что-то автоматизировать, нужно взять тесткейс. И если в нем будет 50-60 строк, то и в автотесте должно быть столько же — сценарий для автотеста не должен кардинально отличаться от мануального. И если в ручном “каждая строка является достаточно сложным шагом”, то надо начать с пересмотра ручных тесткейсов. Что-то мне подсказывает, что даже если в ручном будут все эти 60 шагов, то они с большой долей вероятности должны быть с вложенными. Например

  1. Заполнить таблицу

    1. Заполнить имя

    2. Заполнить фамилию

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

то придётся писать обёртку:

Всё, что у вас представлено — обертки над селенидом и аллюром. Вы переизобрели Page Objects с типизацией, которым более десяти лет назад уже реализовали, привет Html elements, Atlas, JDI.

Все действия в отчёте будут выглядеть одинаково.

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

Что если нам нужно сделать проверку каждого в отдельности из 30 параметров и для каждого создать отдельный метод с описанием? То есть:

Людям дали Selenide, но они упорно тащили ассерты в стиле селениума. element.text() и затем Assertions.assertThat показывает, что вы или не умеете пользоваться Selenide, или не хотите.

И делать так для остальных 29 параметров? Конечно, нет.

Или вы запутались в своих аргументах, или что-то еще случилось. Вы же передаете имя пользователя, по которому делается фильтр. Написанный метод уже является достаточно универсальный. Нужно красивое описание в отчете? Пишите тогда так:

    @Step
    public void assertEmail(String userId, String expectedEmail) {
        Allure.getLifecycle().updateStep(stepResult ->
          stepResult.setName("Проверяем пользователя %s, ожидаем email %s"
                             .formatted(userId, expectedEmail))
        ); 
        users.filter(Condition.text(userId)).first()
          .$x("div[@id='#email-container']")
          .shouldHave(Condition.text(expectedEmail));
    }

Это, очевидно, даст в отчете:

Проверяем пользователя Вася, ожидаем email vasya@vasya.com
Проверяем пользователя Маша, ожидаем email masha@masha.com

А если надо еще и название таблицы в строку шага, так прокидывайте ее через конструктор. И такой вариант более гибкий, чем указывать все в Step над методом.

Ну а если вам не нравятся дополнительные логгируемые шаги Selenide, то для этого есть настройка: new AllureSelenide().includeSelenideSteps(false);

Или же если вам не нужны параметры в шагах, то и их тоже можно удалять. Allure дает широкие возможности для кастомизации.

… сравнивая все параметры посредством Soft Assert. Да, это здоровое решение, но всё равно придётся писать логику, а о красивом автологируемом отчёте можно забыть.

Не, с софт-ассертом очень больное решение. А автологируемый отчет берите выше. И для его реализации не нужно ничего городить дополнительно (кроме оберток поверх инпутов/кнопок/прочего специфичного для каждого UI-проекта).

Если в Selenide для работы со списками используется класс ElementsCollection, то в Allurium для этой цели применяется класс ListWC

При этом класс ListWC использует внутри ElementsCollection, вы просто сделали обертку со своими методами и своей логикой, которая ничего нового не принесла.

Глянул еще внутрь ListWC. Кажется, вы просто принципиально отказываетесь от Selenide, но городите свои костыли, чтобы что? Как я понял, чтобы только вам было понятно и удобно. Но вот только чтобы писать в вашем стиле, нужно потратить немало времени. То есть для перехода от Selenium к Selenide нужно меньше времени, чем для перехода от Selenide к Alluruim. При этом примеры отчетов, которые я вижу, вообще не похожи на реальные кейсы.

Не будет в тесткейсе мануальщик писать "Ввести [Сова] в инпут текстового поля списка полей ввода". Или "Проверить, что значение инпута текстового поля [Login] равно [john]". И руками этот кейс потом тоже проходить больно с таким описанием.

А так последний скрин прекрасно создается кодом, который я показал выше. Без АОП, без дополнительных фреймворков, без ломбока, без Name, без Locator (кстати, не заметил примеров работы с динамическими локаторами) — только на Allure+Selenide.

Так что в качестве личного петпроекта только для одного изолированного автоматизатора, чьи отчеты смотрит только он сам, это прикольно, но для работы в qa-отделе тут пилить и пилить (и получить обратно Selenide с Allure).

Спасибо за конструктивную критику.

Благодарен за свежий взгляд. В ответе к другому посту ниже я частично ответил на вопросы. Постараюсь коротко дать реплику.

 @Step
    public void assertEmail(String userId, String expectedEmail) {
        Allure.getLifecycle().updateStep(stepResult ->
          stepResult.setName("Проверяем пользователя %s, ожидаем email %s"
                             .formatted(userId, expectedEmail))
        ); 
        users.filter(Condition.text(userId)).first()
          .$x("div[@id='#email-container']")
          .shouldHave(Condition.text(expectedEmail));
    }

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

dynamicEmployeesListPage.employees().get("jane.doe").email().assertText("jane.doe@example.com")

1 строчка сценария без десяти строк логики. Остальное собирает структура проекта.

А в отчёте увидим примерно следующее:

Проверяем, что значение [email] пользователя [jane.doe] соответствует "jane.doe@example.com"

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

Пример в документации здесь

При этом класс ListWC использует внутри ElementsCollection, вы просто сделали обертку со своими методами и своей логикой, которая ничего нового не принесла.

Это не совсем так. ListWC действительно отчасти является wrapper. Его главная цель привнести строгую типизацию в коллекции которая и позволяет избавиться он написания кучи шагов с аннотацией @Step и почти забыть о фильтрациях. Типизация заставляет создавать чёткую структуру, взамен даёт кучу контроля которая в итоге и даёт 1 строчку кода вместо 10ти. Я рассматривал решение отказаться от ElementsCollection в основе. Но по сути мне пришлось бы заменять его api почти аналогичным. И после ряда экспериментов это не показало профита. К тому-же цель была доработать selenide, а не отказаться от него. Хотя не всё в ElementsCollection в самом selenide работает без нареканий. Но это другая тема.

Всё, что у вас представлено — обертки над селенидом и аллюром. Вы переизобрели Page Objects с типизацией, которым более десяти лет назад уже реализовали, привет Html elements, Atlas, JDI.

Я не говорил о том что идея совсем уникальна. Не могу много сказать об Atlas, JDI. т.к. пользоваться не довелось. Одной из целей была как и объяснял - взать лучшее от selenide и дообвесить нужным мне функционалом.

Для начала стоит вспомнить, что перед тем, как что-то автоматизировать, нужно взять тесткейс. И если в нем будет 50-60 строк, то и в автотесте должно быть столько же — сценарий для автотеста не должен кардинально отличаться от мануального.

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

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

Не вижу способа как это избавит от написания логики типа "assertEmail(String userId, String expectedEmail)" и даст мультиязычность.

Не, с софт-ассертом очень больное решение. 

Вот здесь не соглашусь. Когда работаешь с кучей таблиц soft assert(ами) в части случаев пользоваться придётся. И для асерта строки написание методов принимающих Data объекты с использованием ofNullable это вполне быстрое и и красивое решение. Которое я встречал довольно часто особенно кстати в BDD подходах. Хотя возможно мы тут недопоняли друг друга.

Глянул еще внутрь ListWC. Кажется, вы просто принципиально отказываетесь от Selenide, но городите свои костыли, чтобы что?

большинство assert методов, встроенных в ListWC из коробки используют selenide ассерты. Лишь где наиболее уместно применить другой assert сделано так, но таких мест меньшинство.

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

Здесь не готов ничего утверждать. У меня мало данных. Пока я мог судить на совсем небольшом колличестве человек. Да структуру мне объяснять пришлось, но с остальным разобрались с помощью среды разработки. Чтобы как-то облегчить это и сделал документацию.

1 строчка сценария без десяти строк логики. Остальное собирает структура проекта.

Ну как без десяти строк? Они у вас просто внутри. Аналогично с моим примером — этот код завернуть в какой-то шаблонный элемент и будет один и тот же код вызываться из разных мест: UsersTable.assertEmail(”jane”, “doe@jane.doe.com”), CustomersTable.assertEmail(”jane2”, “doe2@jane.doe.com”). Можно тоже сказать, что это одна строчка, а остальное собирает структура проекта.

А в отчёте увидим примерно следующее:

Проверяем, что значение [email] пользователя [jane.doe] соответствует "jane.doe@example.com"

На мой взгляд, это очень даже не плохое описание, особенно если учесть, что я вообще ничего для этого не написал.

Только вот люди так тесткейсы не пишут. На строчку тесткейса “Залогиниться под покупателем” у вас будет 10 строк такого роботизированного описания, которое мануальщику читать больно, и в результате вместо общей работы над качеством продукта получаем, что каждый работает для себя. И вот чтобы стало удобно, вам придется все эти формализованные шаги обернуть в step(”Залогиниться под покупателем”, ()→{…}), чтобы на верхнем уровне было описание действия, а на нижнем служебная информация.

Плюс я могу изменить параметр в конфигурации и сразу получить этоже на английском.

А зачем? Ну то есть какой смысл в смене языка? Тесткейсы пишутся на одном языке, так зачем в коде возможность его переключения? Для логов английский, его переключать не надо, для тесткейсов тот, на котором они написаны. Но даже если и надо, то не составит труда в код выше добавить switch/if на все варианты с Language, который будет реагировать на конфиг.

ListWC действительно отчасти является wrapper. Его главная цель привнести строгую типизацию…Типизация заставляет создавать чёткую структуру, взамен даёт кучу контроля которая в итоге и даёт 1 строчку кода вместо 10ти.

Это на самом деле странный и противоречивый аргумент. Типизация это хорошо. Но она не дает контроль, который приведет к сокращению строк. Ваша 1 строчка добавляет кучу аннотаций, АОП, какие-то дополнительные телодвижение, подменяет реализацию Selenide (а скришот делается там, где вы ассерты заменили? а умные ожидания ваши тоже задаются точечно?). Не очень понимаю, в чем проблема с фильтрами в коллекциях. Не помню, чтобы stream api стал нерукопожатным. Инкапсуляция же решает все проблемы. И не понимаю, чем ListWC объективно лучше любого другого класса, который будет принимать внутрь себя ElementsCollection и таким образом тоже приносить строгую типизацию.

В общем, проще написать кодом

  class Table {
    private final String tableName;
    private final SelenideElement tableLocator;
    
    public Table(String tableName, SelenideElement tableLocator) {
      this.tableName = tableName;
      this.tableLocator = tableLocator;
    }
    
    public TableRow getRowByName(String rowName) {
      SelenideElement rowElement = tableLocator.$$(".elements-list")
        .filter(Condition.text(rowName)).first();
      
      return new TableRow(tableName, rowName, rowElement);
    }
  }
  
  class TableRow {
    private final String tableName;
    private final String rowName;
    private final SelenideElement rowElement;
    
    public TableRow(String tableName, String rowName, SelenideElement rowElement) {
      this.tableName = tableName;
      this.rowName = rowName;
      this.rowElement = rowElement;
    }
    
    @Step
    public TableRow assertFieldValue(String fieldName, String expectedValue) {
      Allure.getLifecycle().updateStep(stepResult ->
        stepResult.setName("Таблица (%s), строка (%s), поле (%s), ожидаем (%s)"
          .formatted(tableName, rowName, fieldName, expectedValue)));
      
      rowElement
        .$(".%s".formatted(fieldName))
        .shouldHave(Condition.text(expectedValue));
      return this;
    }
  }

Два класса, в которых поправить локаторы и они будут универсальны для любых таблиц. При этом здесь всё на виду — понятно, как расширять и как пользоваться.

И тест будет примерно как у вас, разве что промежуточных методов меньше, но это +пара классов такой же (никакой) сложности:

new Table("Список покупателей", Selenide.$(".table"))
        .getRowByName("Dmitriy")
        .assertFieldValue("Email", "dima9000@dima.com")
        .assertFieldValue("Name", "Dima");
        
//B отчете будет:
//Таблица (Список покупателей), строка (Dmitriy), поле (Email), ожидаем ([dima9000@dima.com](<mailto:dima9000@dima.com>))
//Таблица (Список покупателей), строка (Dmitriy), поле (Name), ожидаем (Dima)

Выглядит так себе на самом деле, особенно, когда проверок больше. Но вот вариант с софт ассертом из junit5 (хотя это плохо и в целом проблема тесткейса, который надо переписать):

step("Проверить данные покупателя: Email, Name, Age", () -> {
  TableRow rowByName = new Table("Список покупателей", Selenide.$(".table"))
    .getRowByName("Dmitriy");
  assertAll(
    () -> rowByName.assertFieldValue("Email", "dima9000@dima.com"),
    () -> rowByName.assertFieldValue("Name", "Dima"));
});

//B отчете будет
//Проверить данные покупателя: Email, Name
//    Таблица (Список покупателей), строка (Dmitriy), поле (Email), ожидаем (dima9000@dima.com)
//    Таблица (Список покупателей), строка (Dmitriy), поле (Name), ожидаем (Dima)

Все решилось стандартными средствами.

Не вижу способа как это избавит от написания логики типа "assertEmail(String userId, String expectedEmail)" и даст мультиязычность.

Код выше отвечает на это. Ну и кодом .email().assertText() вы не избавились от логики типа assertEmail(String userId, String expectedEmail), вы просто завернули это в другой класс или метод. Код класса TableRow можно аналогично завернуть в метод email()/name()/age()/address(), чтобы не писать каждый раз в assertFieldValue в явном виде "Email". Просто будет кучка методов, делающих return new TableRow(tableName, Имя/Фамилия/Адрес/Возраст, rowElement). Это ровным счетом ничего не меняет. Про мультиязычность аргумент не могу принять, я ни разу не видел, чтобы она хоть где-то встречалась, то есть чтобы внутреннее описание нужно было на двух языках. А шаги из тесткейсов у вас в любом случае должны быть на том же языке.

К тому-же цель была доработать selenide, а не отказаться от него

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

Когда работаешь с кучей таблиц soft assert(ами) в части случаев пользоваться придётся.

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

И ни разу я не видел такого чёткого процесса. Тот кто платит зарплату - тот заказывает и музыку.

Так вы QA или кто? Тот, кто платит зарплату, не знает ни про тестирование, ни про разработку. Он нанимает специалиста. И QA, как специалист, просто обязан улучшать процессы, до которых может дотянуться. Если ваше видение с компанией в этом плане расходится, то зачем работать через силу? Более того, когда компания захочет переходить с вашего подхода на 1 в 1 соответствие тесткейсам, ей потребуется человек, который сможет понять, что вы написали, что уже непросто из-за АОП (которое там не нужно), который будет это править, переписывать абсолютно все ваши автотесты, потому что их и руками пройти сложно. Такой подход делает максимально дорого по всем параметрам. И сравните с моим кодом — он занял минут 10, но легко расширяем, легок в использовании, не требует дополнительных знаний вообще. При этом он дает ту же самую типизацию.

formPage.fieldLogin().assertHasCssClass("form-control");

В прошлый раз еще за это глаз зацепился. Такое в коде тестов — плохо. Вы же тащите наружу в тест методы, которые должны быть спрятаны. Что еще раз говорит о том, что ваши описания шагов — низкоуровневые и должны быть обернуты описаниями из тесткейсов. “The basic rule of thumb for a page object is that it should allow a software client to do anything and see anything that a human can.” — Мартин Фаулер про Page Object.

И давайте в финале сравним ваш код и мой вариант, как это у вас в документации с Selenide. Так будет честнее. А то везде сравнение абстракций разного уровня, что неспортивно.

Ваш вариант с убранными методами, которых у меня нет:

@PageObject
@Getter
@Accessors(fluent = true)
public class DynamicEmployeesListPage extends Page {
    @Name("Employees")
    @ListLocator(css = "#employeeTableBody tr")
    protected ListWC<EmployeeRecord> employees = new ListWC<>();

    @Widget
    @Getter
    @Accessors(fluent = true)
    public static class EmployeeRecord extends AbstractWidget {

        @Name("First name")
        @LocatorChain(xpath = "td[1]")
        protected Value firstName;

        @Name("Age")
        @LocatorChain(xpath = "td[3]")
        protected Value age;

        @Name("Email")
        @LocatorChain(xpath = "td[4]")
        protected Value email;

        public EmployeeRecord(SelenideElement rootElement) {
            super(rootElement);
        }

        @Override
        public String getId() {
            return email.text();
        }
    }
}

А вот мой:

  class TablePage {
    public Table employeeTable() {
      return new Table("Список покупателей", Selenide.$(".table"));
    }
  }
  
  class TableTest {
    @Test
    void test() {
      new TablePage().employeeTable()
        .getRowByName("Dmitriy")
        .assertEmail("dima9000@dima.com")
        .assertFirstName("Dima")
        .assertAge("50");
    }
  } 
  
  
  //в класс TableRow было добавлено три метода
public TableRow assertAge(String expectedAge) {
  return assertFieldValue("Age", expectedAge);
}

public TableRow assertEmail(String expectedEmail) {
  return assertFieldValue("Email", expectedEmail);
}

public TableRow assertFirstName(String expectedFirstName) {
  return assertFieldValue("First name", expectedFirstName);
}  

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

Ну как без десяти строк? Они у вас просто внутри.

Внутри есть шаблон и логика сборки.

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

Нет. Это совершенно не аналогично.
В вашем примере UsersTable.assertEmail(”jane”, “doe@jane.doe.com”) вы написали конкретный случай и конкретную для него логику. И для каждого такого даже похожего, но частично отличающегося случая вы будете писать отдельный уникальный метод с аннотацией @Step. Тоесть малейшее отклонение - что если вставить столбик в таблицу? будете писать новый "assertEmail" с небольшими изменениями? А уменя всё типизировано, всё итак работает. Можно много таких примеров привести, это простейший.

Можно тоже сказать, что это одна строчка

Таким образом за этой одной строкой стоит как минимум 10 строковый метод. Поэтому так сказать нельзя.

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

Насколько я могу понять это один из предметов основного осуждения и критики. Но эта статья посвещена одному из решений которое работает не в идеальном мире. И то как следует писать тест кейсы к данной статье не относиться, потому что это тема отдельная и необъятная, для неё посвящены другие статьи. Здесь же повторюсь - инструмент был создан в первую очередь для автоматизаторов которые работают при минимальной поддержке ручных тестировщиков или без неё. Где есть необходимость делать быстро и читабельно пусть даже в чём-то жертвуя правильностью принципа BDD. Главная цель - не угодить всем, а забустить selenide фичами котрорых мне как разработчику работающему в моих условиях не хватало. Уверен в этом мире в моих условиях работают многие.

На строчку тесткейса “Залогиниться под покупателем” у вас будет 10 строк такого роботизированного описания, которое мануальщику читать больно.
И вот чтобы стало удобно, вам придется все эти формализованные шаги обернуть в step

Никто не мешает создать вам создать ваш метод "login(String login, String password)" если вы так хотите и потом переиспользовать его. И тогда он будет отображаться одной строкой в репорте как вы и хотите. Но разница всё равно будет в том, что подшаги будут выглядеть по-человечески, а не то, что нам выдаст чистый selenide. Однако необходимость группировать несколько действий под одним шагом типо "login(String login, String password)" возникает гораздо реже чем простые одиночные действия. И в том же cucumber основная масса дубликатов рождается из неоходимости оборачивать в шаг любой простейший клиr на любом элементе. От этого я и хотел избавиться.
К тоже вы всегда можете отредактировать шаблоны шагов так, чтобы они наиболее устраивали именно вас. Всё редактируется, ничего не зашито жёстко. Пример есть в документации - https://ipk-tools.github.io/allurium-showcase/eng/examples/report_steps_editing/report_steps_editing.html

А зачем? Ну то есть какой смысл в смене языка? Тесткейсы пишутся на одном языке.

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

Но даже если и надо, то не составит труда в код выше добавить switch/if на все варианты с Language, который будет реагировать на конфиг.

Вот у вас пара тысяч кейсов. Сколько строк кода и дополнительной логики вы добавите своим "switch/if" ?

Типизация это хорошо. Но она не дает контроль, который приведет к сокращению строк.

Возможно в сокращение строк вы вкладываете другое понятие т.к. выше мы уже разобрали ваш пример с методом "assertEmail", где я этот метод даже не пишу, а укладываюсь в 1 строку. Также многие примеры в документации - https://ipk-tools.github.io/allurium-showcase/eng/faq.html

Ваша 1 строчка добавляет кучу аннотаций

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

Ваша 1 строчка добавляет кучу АОП

Единственно необходимая 1 аннотация @Name, при сильном желании можно даже от неё отказаться, но это ухудшит читаемость.
С АОП вы никак не контактируете. Точно так же Allure вам добавляет АОП. Но вы же его не видите правда.

а скришот делается там, где вы ассерты заменили?

По моему опыту скриншот следует настраивать в TestListener основного тестового фреймворка. Тоесть используете например TestNg, вот в его листенере и настраивайте скриншот. Скриншот попадёт автоматически куда настроете. Здесь никаких ограничений, т.к. это отдельный механизм.

а умные ожидания ваши тоже задаются точечно?

Что значит "тоже"? Где же в описании говорилось, что они задаются точечно? У нас есть глобальный параметр как в selenide например "timeout" отсюда у нас и будет задаваться базовый таймаут. Если вам где-то необходимо параметр изменить, можете сделать это так же как в selenide.

Не очень понимаю, в чем проблема с фильтрами в коллекциях.

Разве где-то говорилось, что с ними есть проблема?
Говорилось о том, что я стремился избавиться от конструкций c многоэтажными .filter(Condition.someCondition)

ElementsCollection filteredRows = rows
            .filterBy(visible)
            .filter(row -> !row.$$("td table").isEmpty())
            .filter(row -> row.$$("td table").stream().anyMatch(nestedTable -> {
                ElementsCollection nestedRows = nestedTable.$$("tr");
                if (nestedRows.size() < 2) return false;
                SelenideElement secondCell = nestedTable.$("tr:nth-child(2) td:nth-child(2)");
                return secondCell.exists() && secondCell.getText().contains("Active");
            }));

Чтобы такое вообще почти никогда писать не нужно было.

Не помню, чтобы stream api стал нерукопожатным.

Разве где-то это утверждалось? Покажите пожалуйста, если да, я извинюсь.

И не понимаю, чем ListWC объективно лучше любого другого класса, который будет принимать внутрь себя ElementsCollection и таким образом тоже приносить строгую типизацию.

Мне очень сложно дать реплику касетельно "лучше любого другого" потому, что что значит "любого другого". Всё зависит от конкретных функций в контексте определённо архитектуры. Если можете привести пример, я смогу дать реплику.

В общем, проще написать кодом.

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

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

@Widget
class TableRow extends AbstractWidget {

	@Name("LastName")
	@LocatorChain(xpath = "td[1]")
	private Cell lastname;

	@Name("Phone")
	@LocatorChain(xpath = "td[1]")
	private Cell phone;

	@Name("Phone")
	@LocatorChain(xpath = "td[1]")
	private Cell email 

	public TableRow(SelenideElement item) {
	    super(item);
	}

	public String getId() {
	    return email.text();
	}

}

Класс Table мне создавать вообще не нужно. Обратить внимание, что ячейки строки отмеченные аннотаицей "@LocatorChain" будут брать старт от корневого элемента списка, путь до которых укажем при создании экземпляра ListWC.

Теперь я могу объявить таблицу в PageObject

@Name("Users")
ListWC table = new ListWC<TableRow>(By.cssLocator("table tr"))

Посути на этом всё. Теперь я могу делать так.

tablePage().table().get("dima9000@dima.com").email().assertText("dima9000@dima.com")
tablePage().table().get("dima9000@dima.com").firstName().assertText("Dima")

В чём я вижу профит. Никакой логики мы не написали, кроме переопределения метода "getId". Так можно задать любой уникальный ID для любого виджета. По которому ListWC всегда найдёт нужный нам. таким образом забываем о конструкциях с filter.

На основании сравнения количства кода вашем примере и этом. Считаю это обоснованным подтверждением для опровержения вашего утверждения "не лучше любого другого класса". И то, что объём мы сократили. Кроме того от объекта TableRow мы теперь можем наследоваться и в случае необходимости немного изменять чтобы применять к каки-то похожим таблицам.
Этот код я важу гораздо более читабельным.

Код выше отвечает на это. Ну и кодом .email().assertText() вы не избавились от логики типа assertEmail(String userId, String expectedEmail), вы просто завернули это в другой класс или метод. Код класса TableRow можно аналогично завернуть в метод email()/name()/age()/address(), чтобы не писать каждый раз в assertFieldValue в явном виде "Email". Просто будет кучка методов, делающих return new TableRow(tableName, Имя/Фамилия/Адрес/Возраст, rowElement). Это ровным счетом ничего не меняет.

Это не так. Пожалуйста пересмотрите примеры выше.

Те же ассерты левые зачем-то

Я уже отвечал на этот вопрос.

В общем, проще написать кодом.

Любым кодом можно всё написать, вопрос лишь времени. Здесь мы полностью согласны.

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

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

про софт-ассерты из-за каких-то проверок, то это индикатор плохого тест-дизайна

Здесь можно вернусть к теме о красоте и идеаьности, которая относится к другим разделам.

Он нанимает специалиста. И QA, как специалист, просто обязан улучшать процессы, до которых может дотянуться. Если ваше видение с компанией в этом плане расходится, то зачем работать

Это снова не относится к теме и не следует обсуждать. Могу лишь сказать, что когда-то очень давно я разделял ваши взгляды.

с вашего подхода на 1 в 1 соответствие тесткейсам, ей потребуется человек, который сможет понять, что вы написали, что уже непросто из-за АОП (которое там не нужно), который будет это править

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

И сравните с моим кодом — он занял минут 10

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

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

Извините. Совершенно не согласен, учитывая всё описанные выше.

В прошлый раз еще за это глаз зацепился.

Так можно делать или не делать. А возможность так делать есть и в чистом selenide. Остальное за каждым.

Мартин Фаулер про Page Object.

Я не читал, но могу предположить, что поскольку все люди разные, опыт автора как правило отличается так же как и наш с вами. Это должно обсуждаться в других темах.

И давайте в финале сравним ваш код и мой вариант, как это у вас в документации с Selenide. Так будет честнее. А то везде сравнение абстракций разного уровня, что неспортивно.

Выше я уже привёл этот пример. Только сравнивайте пожалуйста правильно.

Вы привели в сравнение целиком весь мой класс

@PageObject
public class DynamicEmployeesListPage extends Page {
    @Name("Employees")
    @ListLocator(css = "#employeeTableBody tr")
    protected ListWC<EmployeeRecord> employees = new ListWC<>();

    @Widget
    @Getter
    @Accessors(fluent = true)
    public static class EmployeeRecord extends AbstractWidget {

        @Name("First name")
        @LocatorChain(xpath = "td[1]")
        protected Value firstName;

        @Name("Age")
        @LocatorChain(xpath = "td[3]")
        protected Value age;

        @Name("Email")
        @LocatorChain(xpath = "td[4]")
        protected Value email;

        public EmployeeRecord(SelenideElement rootElement) {
            super(rootElement);
        }

        @Override
        public String getId() {
            return email.text();
        }
    }
}

И сравниваете лишь с частью вашего. И на первый взгляд создаётся впечатление, что моё решение длиннее.

Чтобы быть честным нужно взять эквивалентную часть вашего решения, которая будет выглядеть так.

class Table {
    private final String tableName;
    private final SelenideElement tableLocator;
    
    public Table(String tableName, SelenideElement tableLocator) {
      this.tableName = tableName;
      this.tableLocator = tableLocator;
    }
    
    public TableRow getRowByName(String rowName) {
      SelenideElement rowElement = tableLocator.$$(".elements-list")
        .filter(Condition.text(rowName)).first();
      
      return new TableRow(tableName, rowName, rowElement);
    }
  }
  
  class TableRow {
    private final String tableName;
    private final String rowName;
    private final SelenideElement rowElement;
    
    public TableRow(String tableName, String rowName, SelenideElement rowElement) {
      this.tableName = tableName;
      this.rowName = rowName;
      this.rowElement = rowElement;
    }
    
    @Step
    public TableRow assertFieldValue(String fieldName, String expectedValue) {
      Allure.getLifecycle().updateStep(stepResult ->
        stepResult.setName("Таблица (%s), строка (%s), поле (%s), ожидаем (%s)"
          .formatted(tableName, rowName, fieldName, expectedValue)));
      
      rowElement
        .$(".%s".formatted(fieldName))
        .shouldHave(Condition.text(expectedValue));
      return this;
    }

    public TableRow assertAge(String expectedAge) {
      return assertFieldValue("Age", expectedAge);
    }

    public TableRow assertEmail(String expectedEmail) {
      return assertFieldValue("Email", expectedEmail);
    }

    public TableRow assertFirstName(String expectedFirstName) {
      return assertFieldValue("First name", expectedFirstName);
    }  
}

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

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

На оснавании чего это утверждается, я не понимаю. Если все примеры выше говорят, что это не так. А какой вообще фреймворк показывает внутреннюю реализацию и темболее зачем?

Попытаюсь донести мысль ещё раз. Моей целью было разработка тестов по принципу

Declaration Driven Development - тоесть объявляй и используй, а не объявляй, пиши логику и используй.

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

Не работал с Selenide, но работал с Selenium и какой-то очень старой версией (или даже форком) Cucumber. Но что не изменилось в Огурце, так это его понятный тестировщикам язык. У нас в проекте все тестовые скрипты (feature-файлы, несколько тысяч) и писали сами тестировщики.

А уж как сделать передачу параметров, сохранение значений, сложные составные шаги, циклы и прочее - вот для всего этого и нужен программист SDET, который расширяет функционал Огурца там, где нужно. Например, у нас были реализованы и переменные с передачей значений между шагами, и всякие циклы, и огуречные макросы-подпрограммы (одних сложных многоуровневых макросов тестировщики написали более тысячи), и выполнение произвольного JavaScript в браузере, и regex'ы, и даже распараллеливание тестов сценария на несколько нодов. Уже не говорю про сложную работу с CSV-файлами, базой данных и так далее.

Ваши же тесты можете писать только вы - тестировщики будут сильно сопротивляться.

Ну и не смотрели ли на современных конкурентов для тестирования веба?

У меня не было никогда цели пересаживать людей на другие инструменты и доказывать что мои решения лучше. Идеальных инструметов я никогда не видел и мои не исключение. Я ориентировался исключительно на облегчении работы для себе лично и нескольких людей которым данный подход очень зашёл, а значит есть вероятность что подойдёт кому-то ещё. Для этих целей и были представлены все материалы. Чтобы оно не пропало когда-нибудь вместе с моими носителями. Ну и разумеется услышать критику.

 У нас в проекте все тестовые скрипты (feature-файлы, несколько тысяч) и писали сами тестировщики.

Ваши же тесты можете писать только вы - тестировщики будут сильно сопротивляться.

У этого инструмента нет цели для чтого чтобы использоваться ручными тестировциками. Точно так же как они не будут писать на Selenide. Изначальная цель была - упроцение работы именно разработчиков-автоматизаторов как я и писал в посте.Чтобы:

  • Писать как-бы на selenide. Но как-то по человечески причесать отчёты.

  • Добавить мультиязычность.

  • Насколько возможно избавиться от нагромождений с фильтрациями.

  • Сделать чёткую иерархию, где например элемент в виджете может может сам брать и достраивать свой локатор стартуя от корня виджета и т.д. (страшное подобие такой фичи присутствует в Serenity)

Ну и не смотрели ли на современных конкурентов для тестирования веба?

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

Sign up to leave a comment.

Articles