Статьи

JUnit 5 и Selenium — Использование встроенного в PageFactory Selenium для реализации шаблона объекта страницы

Selenium — это набор инструментов и библиотек, поддерживающих автоматизацию браузера, и он в основном используется для тестирования веб-приложений. Одним из компонентов Selenium является Selenium WebDriver, который предоставляет клиентскую библиотеку, проводной протокол JSON (протокол для связи с драйверами браузера) и драйверы браузера. Одним из основных преимуществ Selenium WebDriver является то, что он поддерживается всеми основными языками программирования и может работать во всех основных операционных системах.

В этой части JUnit 5 с Selenium WebDriver — Учебник я расскажу о реализации шаблона Page Object с помощью встроенного класса поддержки Selenium PageFactory. PageFactory предоставляет механизм для инициализации любого объекта Page, который объявляет WebElement или List<WebElement> с аннотацией @FindBy .

Об этом уроке

Вы читаете вторую часть JUnit 5 с Selenium WebDriver — Tutorial .

Все статьи в этом уроке:

Далее:

  • Часть 3. Улучшение конфигурации проекта — параллельное выполнение тестов, порядок выполнения тестов, параметризованные тесты, AssertJ и многое другое

Исходный код этого руководства можно найти на Github.

Представление шаблона объекта страницы

Мы будем создавать тесты для JavaScript-приложения Todo, доступного здесь: http://todomvc.com/examples/vanillajs . Приложение создается как одностраничное приложение (SPA) и использует локальное хранилище в качестве хранилища задач. Возможные сценарии, которые должны быть реализованы, включают добавление и редактирование задачи, удаление задачи, маркировку одного или нескольких задач как выполненных. Реализация будет выполнена с использованием шаблона Page Object.

Целью шаблона Page Object является абстрагирование страниц приложения и функциональности от реальных тестов. Шаблон Page Object улучшает повторное использование кода в тестах и ​​тестах, но также облегчает поддержку кода.

Вы можете прочитать больше об этом шаблоне в статье Мартина Фаулера: https://martinfowler.com/bliki/PageObject.html

API страницы или объект страницы

Мы начнем проект с моделирования страницы TodoMVC как объекта страницы. Этот объект будет представлять API страницы, который будет использоваться в тестах. Сам API может быть смоделирован с использованием интерфейса. Если вы посмотрите на методы приведенного ниже интерфейса, то заметите, что эти методы — это просто пользовательские функции, доступные на странице. Пользователь может создать задачу , пользователь может переименовать задачу или он может удалить задачу :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public interface TodoMvc {
    void navigateTo();
    void createTodo(String todoName);
    void createTodos(String... todoNames);
    int getTodosLeft();
    boolean todoExists(String todoName);
    int getTodoCount();
    List<String> getTodos();
    void renameTodo(String todoName, String newTodoName);
    void removeTodo(String todoName);
    void completeTodo(String todoName);
    void completeAllTodos();
    void showActive();
    void showCompleted();
    void clearCompleted();
}

Приведенный выше интерфейс (очевидно) скрывает все детали реализации, но также не предоставляет никаких подробностей Selenium WebDriver потенциальному клиенту (в нашем случае клиент = тестовый метод). На самом деле, он не имеет никакого отношения к Selenium WebDriver. Таким образом, теоретически мы могли бы иметь разные реализации этой страницы для разных устройств (например, мобильное нативное приложение, настольное приложение и веб-приложение).

Создание тестов

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

Были созданы следующие тесты:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@ExtendWith(SeleniumExtension.class)
@DisplayName("Managing Todos")
class TodoMvcTests {
 
    private TodoMvc todoMvc;
 
    private final String buyTheMilk = "Buy the milk";
    private final String cleanupTheRoom = "Clean up the room";
    private final String readTheBook = "Read the book";
 
    @BeforeEach
    void beforeEach(ChromeDriver driver) {
        this.todoMvc = null;
        this.todoMvc.navigateTo();
    }
 
    @Test
    @DisplayName("Creates Todo with given name")
    void createsTodo() {
 
        todoMvc.createTodo(buyTheMilk);
 
        assertAll(
                () -> assertEquals(1, todoMvc.getTodosLeft()),
                () -> assertTrue(todoMvc.todoExists(buyTheMilk))
        );
    }
 
    @Test
    @DisplayName("Creates Todos all with the same name")
    void createsTodosWithSameName() {
 
        todoMvc.createTodos(buyTheMilk, buyTheMilk, buyTheMilk);
 
        assertEquals(3, todoMvc.getTodosLeft());
 
 
        todoMvc.showActive();
 
        assertEquals(3, todoMvc.getTodoCount());
    }
 
    @Test
    @DisplayName("Edits inline double-clicked Todo")
    void editsTodo() {
 
        todoMvc.createTodos(buyTheMilk, cleanupTheRoom);
 
        todoMvc.renameTodo(buyTheMilk, readTheBook);
 
        assertAll(
                () -> assertFalse(todoMvc.todoExists(buyTheMilk)),
                () -> assertTrue(todoMvc.todoExists(readTheBook)),
                () -> assertTrue(todoMvc.todoExists(cleanupTheRoom))
        );
    }
 
    @Test
    @DisplayName("Removes selected Todo")
    void removesTodo() {
 
        todoMvc.createTodos(buyTheMilk, cleanupTheRoom, readTheBook);
 
        todoMvc.removeTodo(buyTheMilk);
 
        assertAll(
                () -> assertFalse(todoMvc.todoExists(buyTheMilk)),
                () -> assertTrue(todoMvc.todoExists(cleanupTheRoom)),
                () -> assertTrue(todoMvc.todoExists(readTheBook))
        );
    }
 
    @Test
    @DisplayName("Toggles selected Todo as completed")
    void togglesTodoCompleted() {
        todoMvc.createTodos(buyTheMilk, cleanupTheRoom, readTheBook);
 
        todoMvc.completeTodo(buyTheMilk);
        assertEquals(2, todoMvc.getTodosLeft());
 
        todoMvc.showCompleted();
        assertEquals(1, todoMvc.getTodoCount());
 
        todoMvc.showActive();
        assertEquals(2, todoMvc.getTodoCount());
    }
 
    @Test
    @DisplayName("Toggles all Todos as completed")
    void togglesAllTodosCompleted() {
        todoMvc.createTodos(buyTheMilk, cleanupTheRoom, readTheBook);
 
        todoMvc.completeAllTodos();
        assertEquals(0, todoMvc.getTodosLeft());
 
        todoMvc.showCompleted();
        assertEquals(3, todoMvc.getTodoCount());
 
        todoMvc.showActive();
        assertEquals(0, todoMvc.getTodoCount());
    }
 
    @Test
    @DisplayName("Clears all completed Todos")
    void clearsCompletedTodos() {
        todoMvc.createTodos(buyTheMilk, cleanupTheRoom);
        todoMvc.completeAllTodos();
        todoMvc.createTodo(readTheBook);
 
        todoMvc.clearCompleted();
        assertEquals(1, todoMvc.getTodosLeft());
 
        todoMvc.showCompleted();
        assertEquals(0, todoMvc.getTodoCount());
 
        todoMvc.showActive();
        assertEquals(1, todoMvc.getTodoCount());
    }
}

Подробнее: Если вы новичок в JUnit 5, вы можете прочитать это введение в моем блоге: https://blog.codeleak.pl/2017/10/junit-5-basics.html . Существует также более новая версия этой статьи, написанная на польском языке: https://blog.qalabs.pl/junit/junit5-pierwsze-kroki/ .

В приведенном выше тестовом классе мы видим, что перед каждым тестом ChromeDriver инициализируется и внедряется в метод настройки ( @BeforeEach ) расширением Selenium Jupiter (отсюда и @ExtendWith(SeleniumExtension.class) ). Объект драйвера будет использоваться для инициализации объекта страницы.

Существуют различные методы моделирования объектов страницы, и многое зависит от характеристик проекта, над которым вы работаете. Вы можете использовать интерфейсы, но это не обязательно. Возможно, вы захотите рассмотреть моделирование на немного более низком уровне абстракции, где API предоставляет более подробные методы, такие как, например, setTodoInput(String value) , clickSubmitButton() .

Использование встроенной в PageFactory Selenium для реализации шаблона объекта Page

На данный момент у нас есть интерфейс, который моделирует поведение страницы TodoMVC, и у нас есть провальные тесты, которые используют API. Следующим шагом является реализация объекта страницы. Для этого мы будем использовать встроенный класс Selenium PageFactory и его утилиты.

Класс PageFactory упрощает реализацию шаблона Page Object. Класс предоставляет механизм для инициализации любого объекта Page, который объявляет WebElement или List<WebElement> @FindBy аннотацией @FindBy . PageFactory и все другие аннотации, поддерживающие реализацию шаблона Page Object, доступны в пакете org.openqa.selenium.support .

Приведенный ниже класс TodoMvcPage реализует интерфейс, который мы создали ранее. Он объявляет несколько полей с аннотацией @FindBy . Он также объявляет конструктор, принимающий параметр WebDriver используемый фабрикой для инициализации полей:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
public class TodoMvcPage implements TodoMvc {
 
    private final WebDriver driver;
 
    private static final By byTodoEdit = By.cssSelector("input.edit");
    private static final By byTodoRemove = By.cssSelector("button.destroy");
    private static final By byTodoComplete = By.cssSelector("input.toggle");
 
    @FindBy(className = "new-todo")
    private WebElement newTodoInput;
 
    @FindBy(css = ".todo-count > strong")
    private WebElement todoCount;
 
    @FindBy(css = ".todo-list li")
    private List<WebElement> todos;
 
    @FindBy(className = "toggle-all")
    private WebElement toggleAll;
 
    @FindBy(css = "a[href='#/active']")
    private WebElement showActive;
 
    @FindBy(css = "a[href='#/completed']")
    private WebElement showCompleted;
 
    @FindBy(className = "clear-completed")
    private WebElement clearCompleted;
 
    public TodoMvcPage(WebDriver driver) {
        this.driver = driver;
    }
 
    @Override
    public void navigateTo() {
        driver.get("http://todomvc.com/examples/vanillajs");
    }
 
    public void createTodo(String todoName) {
        newTodoInput.sendKeys(todoName + Keys.ENTER);
    }
 
    public void createTodos(String... todoNames) {
        for (String todoName : todoNames) {
            createTodo(todoName);
        }
    }
 
    public int getTodosLeft() {
        return Integer.parseInt(todoCount.getText());
    }
 
    public boolean todoExists(String todoName) {
        return getTodos().stream().anyMatch(todoName::equals);
    }
 
    public int getTodoCount() {
        return todos.size();
    }
 
    public List<String> getTodos() {
        return todos
                .stream()
                .map(WebElement::getText)
                .collect(Collectors.toList());
    }
 
    public void renameTodo(String todoName, String newTodoName) {
        WebElement todoToEdit = getTodoElementByName(todoName);
        doubleClick(todoToEdit);
 
        WebElement todoEditInput = find(byTodoEdit, todoToEdit);
        executeScript("arguments[0].value = ''", todoEditInput);
 
        todoEditInput.sendKeys(newTodoName + Keys.ENTER);
    }
 
    public void removeTodo(String todoName) {
        WebElement todoToRemove = getTodoElementByName(todoName);
        moveToElement(todoToRemove);
        click(byTodoRemove, todoToRemove);
    }
 
    public void completeTodo(String todoName) {
        WebElement todoToComplete = getTodoElementByName(todoName);
        click(byTodoComplete, todoToComplete);
    }
 
    public void completeAllTodos() {
        toggleAll.click();
    }
 
    public void showActive() {
        showActive.click();
    }
 
    public void showCompleted() {
        showCompleted.click();
    }
 
    public void clearCompleted() {
        clearCompleted.click();
    }
 
    private WebElement getTodoElementByName(String todoName) {
        return todos
                .stream()
                .filter(el -> todoName.equals(el.getText()))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Todo with name " + todoName + " not found!"));
    }
 
    private WebElement find(By by, SearchContext searchContext) {
        return searchContext.findElement(by);
    }
 
    private void click(By by, SearchContext searchContext) {
        WebElement element = searchContext.findElement(by);
        element.click();
    }
 
    private void moveToElement(WebElement element) {
        new Actions(driver).moveToElement(element).perform();
    }
 
    private void doubleClick(WebElement element) {
        new Actions(driver).doubleClick(element).perform();
    }
 
    private void executeScript(String script, Object... arguments) {
        ((JavascriptExecutor) driver).executeScript(script, arguments);
    }
}

@FindBy — не единственная аннотация, используемая для поиска элементов в объекте страницы. Есть также @FindBys и @FindAll .

@FindBys

Аннотация @FindBys используется для пометки поля в объекте страницы, чтобы указать, что поиск должен использовать серию тегов @FindBy . В этом примере Selenium будет искать элемент с class = "button" который находится внутри элемента с id = "menu" :

1
2
3
4
5
@FindBys({
  @FindBy(id = "menu"),
  @FindBy(className = "button")
})
private WebElement element;

@FindAll

Аннотация @FindAll используется для пометки поля в объекте страницы, чтобы указать, что поиск должен использовать серию тегов @FindBy. В этом примере Selenium будет искать все элементы с class = "button" и все элементы с id = "menu" . Элементы не гарантируются в порядке документов:

1
2
3
4
5
@FindAll({
  @FindBy(id = "menu"),
  @FindBy(className = "button")
})
private List<WebElement> webElements;

PageFactory — инициализировать объект Page

PageFactory предоставляет несколько статических методов для инициализации объектов страницы. В нашем тесте в beforeEach() нам нужно инициализировать объект TodoMvcPage :

1
2
3
4
5
@BeforeEach
void beforeEach(ChromeDriver driver) {
    this.todoMvc = PageFactory.initElements(driver, TodoMvcPage.class);
    this.todoMvc.navigateTo();
}

PageFactory инициализирует объект с помощью отражения, а затем инициализирует все WebElement или List<WebElement> помеченные аннотацией @FindBy (поиск в этом моменте не выполняется, поля проксируются). Для использования этого метода необходимо, чтобы объект Page Object имел один конструктор параметров, принимающий объект WebDriver .

Расположение элементов

Так, когда элементы расположены? Поиск происходит каждый раз при доступе к полю. Так, например, когда мы выполняем код: newTodoInput.sendKeys(todoName + Keys.ENTER); в createTodo() выполняется фактическая инструкция: driver.findElement(By.className('new-todo')).sendKeys(todoName + Keys.ENTER) . Можно ожидать, что потенциальное исключение, при котором элемент не был найден, генерируется не во время инициализации объекта, а во время поиска первого элемента.

Selenium использует шаблон Proxy для достижения описанного поведения.

@CacheLookup

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

1
2
3
@FindBy(className = "new-todo")
@CacheLookup
private WebElement newTodoInput;

Запуск тестов

Самое время выполнить тесты. Это можно сделать либо из IDE, либо с помощью терминала:

1
./gradlew clean test --tests *TodoMvcTests

Сборка прошла успешно, все тесты пройдены:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
> Task :test
 
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > editsTodo() PASSED
 
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > togglesTodoCompleted() PASSED
 
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > createsTodo() PASSED
 
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > removesTodo() PASSED
 
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > togglesAllTodosCompleted() PASSED
 
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > createsTodosWithSameName() PASSED
 
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > clearsCompletedTodos() PASSED
 
BUILD SUCCESSFUL in 27s
3 actionable tasks: 3 executed

Следующие шаги

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

См. Оригинальную статью здесь: JUnit 5 и Selenium — Использование встроенного Selenium `PageFactory` для реализации шаблона объекта страницы

Мнения, высказанные участниками Java Code Geeks, являются их собственными.