Заполнение простой формы

Пример 1

Начинать всегда проще с простых примеров.

Давайте для начала рассмотрим один из простейших сценариев, такой как заполнение примитивной формы, с разными типами полей ввода. Код страницы лежит в проекте с примерами. Форма выглядит так:



Сначала рассмотрим один из вариантов, как это могло бы быть сделано на Selenide

Будем использовать JUnit, т.к. он максимально прост, а сложный функционал здесь не требуется.

1. Нужно создать Page Object и описать имеющиеся на странице элементы.

@PageObject
@Getter
@Accessors(fluent = true)
public class FormPage extends Page {

    protected TopBar topBar = new TopBar();
    protected SelenideElement fieldLogin = $("#login").as("Textfield Login");
    protected SelenideElement fieldEmail = $("#email").as("Textfield Email");
    protected SelenideElement fieldPassword = $("#password").as("Textfield Password");
    protected SelenideElement fieldRank = $("#rank").as("Textfield rank");
    protected SelenideElement fieldDate = $x("//input[@id='date']").as("Textfield Date");
    protected SelenideElement fieldTelephone = $("#tel").as("Textfield Telephone");
    protected SelenideElement uploadAvatar = $("#file").as("File input Avatar");
    protected SelenideElement radioBtnMale = $("#male").as("Radiobutton Gender Male");
    protected SelenideElement radioBtnFemale = $("#female").as("Radiobutton Gender Female");
    protected SelenideElement ckbMorning = $("#checkbox1").as("Checkbox Preferable time - Morning");
    protected SelenideElement ckbEvening = $("#checkbox2").as("Checkbox Preferable time - Evening");
    protected SelenideElement ddExperience = $("#experience").as("Dropdown list Experience");
    protected SelenideElement selectWorkingDays = $("#multiple-select").as("Select Working Days");
    protected SelenideElement btnReset = $(".btn-secondary").as("Button Reset");
    protected SelenideElement btnSubmit = $(".btn-primary").as("Button Submit");
}

2. Написать сам тестовый сценарий в соответствующем классе.

@Epic("Selenide style")
public class ITExampleSelenideTest extends TestBaseSelenide {

    @Test
    @Feature("Form")
    @DisplayName("Filling the example form")
    protected void fillTheForm() {
        URL imageUrl = this.getClass().getClassLoader().getResource("img/testicon.png");
        Selenide.open(formPageUrl);
        formPage.fieldLogin().shouldBe(Condition.visible);
        formPage.fieldLogin().sendKeys("John");
        formPage.fieldLogin().shouldHave(Condition.cssClass("form-control"));
        formPage.fieldLogin().shouldHave(Condition.value("John"));
        formPage.fieldEmail().sendKeys("john.doe@gmail.com");
        formPage.fieldEmail().shouldHave(Condition.value("john.doe@gmail.com"));
        formPage.fieldPassword().sendKeys("Password12345!");
        formPage.fieldRank().sendKeys("10");
        formPage.fieldDate().sendKeys("11.11.2011");
        formPage.fieldTelephone().sendKeys("199887688");
        formPage.uploadAvatar().uploadFile(new File(imageUrl.getFile()));
        formPage.radioBtnMale().click();
        formPage.radioBtnMale().shouldBe(Condition.enabled);
        formPage.radioBtnFemale().shouldBe(Condition.disabled);
        formPage.ckbMorning().click();
        formPage.ckbMorning().shouldBe(Condition.checked);
        formPage.ckbEvening().shouldNotBe(Condition.checked);
        formPage.ddExperience().selectOption("2 years");
        formPage.selectWorkingDays().selectOption("Monday", "Friday");
        formPage.btnSubmit().click();
    }
}

Скрипт прост и понятен. Мы обозначили имена полей и заполнили их. Казалось бы, нечего улучшать. Давайте теперь сгенерируем отчёт Allure и посмотрим:

Отчёт Selenide

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

К тому же, что делать, если, как в одной из компаний, отчёт хотят видеть на другом языке? С этим есть проблема.

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

Я хотел автоматически получать примерно такой отчёт:

Отчёт Allurium

В этом отчёте обозначены все типы элементов, а человекопонятным языком описывается совершаемое действие. Видны все применённые ассерты, но отсутствует лишний мусор. Такой отчёт я получаю автоматически, если тест составлен похожим образом:

@Epic("Selenide style")
public class ITExampleAlluriumTest extends TestBaseAllurium {
    @Test
    @Feature("Form")
    @DisplayName("Filling the example form")
    public void fillTheForm() {
        URL imageUrl = this.getClass().getClassLoader().getResource("img/testicon.png");
        UiSteps.openBrowser(formPageUrl);
        formPage.fieldLogin().assertVisible();
        formPage.fieldLogin().write("John");
        formPage.fieldLogin().assertHasCssClass("form-control");
        formPage.fieldLogin().assertCurrentValue("John");
        formPage.fieldEmail().write("john.doe@gmail.com");
        formPage.fieldEmail().assertCurrentValue("john.doe@gmail.com");
        formPage.fieldPassword().write("Password12345!");
        formPage.fieldRank().write("10");
        formPage.fieldDate().clearAndWrite("11.11.2011");
        formPage.fieldTelephone().write("199887688");
        formPage.uploadAvatar().uploadFile(new File(imageUrl.getFile()));
        formPage.radioBtnMale().click();
        formPage.radioBtnMale().assertEnabled();
        formPage.radioBtnFemale().assertDisabled();
        formPage.ckbMorning().check();
        formPage.ckbMorning().assertChecked();
        formPage.ckbEvening().assertUnchecked();
        formPage.ddExperience().select("2 years");
        formPage.selectWorkingDays().select("Monday");
        formPage.selectWorkingDays().select("Friday");
        formPage.btnSubmit().click();
    }
}

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

PageObject для Allurium

Для создания PageObject в случае использования Allurium подход не сильно меняется. Всё должно быть чётко структурировано и типизировано, чтобы каждый вид элемента имел свой правильный тип, а фреймворк понимал, как с ним работать, и составлял корректное описание в отчёте.

@PageObject
@Getter
@Accessors(fluent = true)
public class FormPage extends Page {

    @Name("Login")
    @Locator(css = "#login")
    protected TextField fieldLogin;

    @Name("Email")
    @Locator(css = "#email")
    protected TextField fieldEmail;

    @Name("Password")
    @Locator(css = "#password")
    protected TextField fieldPassword;

    @Name("Rank")
    protected TextField fieldRank = $textField("#rank");

    @Name("Date")
    protected TextField fieldDate = _$textField("//input[@id='date']");

    @Name("Telephone")
    @Locator(css = "#tel")
    protected TextField fieldTelephone;

    @Name("Avatar")
    @Locator(css = "#file")
    protected UploadField uploadAvatar;

    @Name("Gender Male")
    @Locator(css = "#male")
    protected Button radioBtnMale;

    @Name("Gender Female")
    @Locator(css = "#female")
    protected Button radioBtnFemale;

    @Name("Preferable time - Morning")
    @Locator(css = "#checkbox1")
    protected CheckBox ckbMorning;

    @Name("Preferable time - Evening")
    @Locator(css = "#checkbox2")
    protected CheckBox ckbEvening;

    @Name("Experience")
    @Locator(css = "#experience")
    protected DropdownSelect ddExperience;

    @Name("Working Days")
    @Locator(css = "#multiple-select")
    protected Select selectWorkingDays;

    @Name("Reset")
    @Locator(css = ".btn-secondary")
    protected Button btnReset;

    @Name("Submit")
    @Locator(css = ".btn-primary")
    protected Button btnSubmit;
}

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

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

Внутри нашего PageObject FormPage располагаются простые элементы. Не все элементы имеют тип SelenideElement — каждый элемент имеет соответствующий ему тип:

Объявление элемента

При объявлении элемента, кроме типа, нужно всегда присваивать ему имя. Я предпочитаю использовать аннотацию @Name("name") вместо вызова .as("name"). Это позволяет создавать экземпляры в нужный момент без лишних сложностей и выглядит более эстетично. Однако можно присваивать имя и через конструктор объекта.

Например, вместо:

@Name("Submit")
@Locator(css = ".btn-primary")
protected Button btnSubmit;

можно написать:

protected Button btnSubmit = $button(".btn-primary", "Submit");

Либо, если экземпляр уже создан, через setName("some-name"):

Button btnSubmit = $button(".btn-primary");
btnSubmit.setName("Submit");

Далее необходимо указать локатор. Это можно сделать разными способами:

  1. Подобно тому, как делали это с SelenideElement:

    @Name("Submit")
    protected Button btnSubmit = $button(".btn-primary");
    

    Где ".btn-primary" — CSS-селектор кнопки. Примитивные элементы, такие как кнопки, текстовые поля и блоки текста, уже описаны во фреймворке. Экземпляр создаётся статическим методом, аналогично созданию SelenideElement:

    protected SelenideElement btnSubmit = Selenide.$(".btn-primary").as("Submit");
    

    При этом Selenide.$ заменяется на просто $ (импортируется как статический метод).

    Длинный вариант был бы:

    @Name("Submit")
    protected Button btnSubmit = Button.$button(".btn-primary");
    

    А короткий вариант через static метод:

    @Name("Submit")
    protected Button btnSubmit = $button(".btn-primary");
    

    Если перед символом $ поставить нижнее подчёркивание (_), как в примере ниже, элемент будет искаться по XPath:

    @Name("Date")
    protected TextField fieldDate = _$textField("//input[@id='date']");
    
  2. Можно создать экземпляр через конструктор:

    @Name("Submit")
    protected Button btnSubmit = new Button(".btn-primary");
    
  3. Или с помощью аннотации @Locator, аналогично @FindBy в Selenium:

    @Name("Submit")
    @Locator(css = ".btn-primary")
    protected Button btnSubmit;
    

    В этом случае экземпляр объявлять не обязательно — он автоматически создаётся фреймворком, если у элемента присутствует аннотация @Locator.

    @Locator может принимать следующие типы локаторов:

    • css — эквивалент By.cssSelector(".btn-primary")
    • xpath — эквивалент By.xpath("//*[@class='btn-primary']")
    • className — эквивалент By.className("btn-primary")
    • id — эквивалент By.id("#id")

Для открытия браузера и загрузки страницы вместо Selenide.open("url"); используется UiSteps.openBrowser(url); — этот шаг также записывается в отчёт. UiSteps — класс со статическими методами, не привязанными к конкретной странице или элементу, которые фреймворк трактует как шаги и логирует в отчёт.

Вызов и логирование шагов

В данной системе каждый элемент имеет набор базовых и специфичных методов (шагов), которые при вызове логируются фреймворком как шаги отчёта. Например, методы contextClick() и doubleClick() доступны для любого элемента, а метод write("some text") присутствует только там, где возможен ввод текста (например, у TextField или TextArea).

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