Home » Java » Enterprise Java » JUnit 5 and Selenium – Using Selenium built-in `PageFactory` to implement Page Object Pattern

About Rafal Borowiec

Rafal Borowiec
Software developer, Team Leader, Agile practitioner, occasional blogger, lecturer. Open Source enthusiast, quality oriented and open-minded.

JUnit 5 and Selenium – Using Selenium built-in `PageFactory` to implement Page Object Pattern

Selenium is a set of tools and libraries supporting browser automation and it is mainly used for web applications testing. One of the Selenium’s components is a Selenium WebDriver that provides client library, the JSON wire protocol (protocol to communicate with the browser drivers) and browser drivers. One of the main advantages of Selenium WebDriver is that it supported by all major programming languages and it can run on all major operating systems.

In this part of the JUnit 5 with Selenium WebDriver – Tutorial I will go though the implementation of Page Object pattern with Selenium’s built-in PageFactory support class. PageFactory provides mechanism to initialize any Page Object that declares WebElement or List<WebElement> fields annotated with @FindBy annotation.

About this Tutorial

You are reading the second part of the JUnit 5 with Selenium WebDriver – Tutorial.

All articles in this tutorial:

Coming up next:

  • Part 3 – Improving the project configuration – executing tests in parallel, tests execution order, parameterized tests, AssertJ and more

The source code for this tutorial can be found on Github

Introducing Page Object Pattern

We will be creating tests for JavaScript based Todo application available here: http://todomvc.com/examples/vanillajs. The application is created as a Single Page Application (SPA) and uses local storage as a task repository. The possible scenarios to be implemented include adding and editing todo, removing todo, marking single or multiple todos as done. The implementation will be done using Page Object pattern.

The goal of Page Object pattern is to abstract the application pages and functionality from the actual tests. Page Object pattern improves re-usability of the code across tests and fixtures but also makes the code easier to maintain.

You can read more about this pattern in the Martin Fowler article: https://martinfowler.com/bliki/PageObject.html

Page API aka Page Object

We will start the project from modelling the TodoMVC page as Page Object. This object will be representing the page API that will be used in tests. The API itself can be modelled using an interface. If you look at the methods of the below interface you notice that the methods are just user functions that are available on the page. User can create todo, user can rename todo or he can remove todo:

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();
}

The above interface (obviously) hides all the implementation details but also it does not expose any Selenium WebDriver details to the potential client (in our case the client = the test method). In fact, it has no relation to Selenium WebDriver whatsoever. So in theory, we could have different implementations of this page for different devices (e.g. mobile native application, desktop application and web application).

Creating tests

With the page API defined we can jump directly to creating the test methods. We will work on the page implementation after we confirm the API can be used for creating tests. This design technique allows to focus on the real usage of the application instead of jumping into implementation details too early.

The following tests were created:

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());
    }
}

More: If you are new to JUnit 5 you can read this introduction on my blog: https://blog.codeleak.pl/2017/10/junit-5-basics.html. There is also a newer version of this article written in polish: https://blog.qalabs.pl/junit/junit5-pierwsze-kroki/.

In the above test class we see that before each test the ChromeDriver is initialized and injected into the setup method (@BeforeEach) by the Selenium Jupiter extension (hence the @ExtendWith(SeleniumExtension.class)). The driver object will be be used to initialize the page object.

There are different page objects modelling techniques and a lot depends on the characteristics of the project you are working on. You may want to use interfaces but it is not required. You may want to consider modelling on a bit lower level of abstraction, where the API is exposing more detailed methods like for example setTodoInput(String value), clickSubmitButton().

Using Selenium built-in PageFactory to implement Page Object Pattern

As of now we have an interface that models the behaviour of the TodoMVC page and we have the failing tests that are using the API. The next step is to actually implement the page object. In order to do so, we will use Selenium built-in PageFactory class and its utilities.

PageFactory class simplifies implementation of Page Object pattern. The class provides mechanism to initialize any Page Object that declares WebElement or List<WebElement> fields annotated with @FindBy annotation. The PageFactory and all other annotations supporting implementation of Page Object pattern are available in the org.openqa.selenium.support package.

The below TodoMvcPage class implements the interface we created earlier. It declares several fields annotated with @FindBy annotation. It also declares a constructor taking WebDriver parameter used by the factory to initialize the fields:

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 is not the only annotation used to lookup elements in a Page Object. There are also @FindBys and @FindAll.

@FindBys

@FindBys annotation is used to mark a field on a Page Object to indicate that lookup should use a series of @FindBy tags. In this example, Selenium will search for the element with class = "button" that is inside the element with id = "menu":

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

@FindAll

@FindAll annotation is used to mark a field on a Page Object to indicate that lookup should use a series of @FindBy tags. In this example, Selenium will search for all the elements with class = "button" and all the elements with id = "menu". Elements are not guaranteed to be in document order:

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

PageFactory – initialize the Page object

PageFactory provides several static methods to initialize Page Objects. In our test, in beforeEach() method we need to initialize TodoMvcPage object:

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

The PageFactory initializes the object using reflection and then it initializes all the WebElement or List<WebElement> fields marked with @FindBy annotation (no lookup is done at this momment, fields are proxied). Using this method requires that the Page Object has a single parameter constructor accepting WebDriver object.

Locating elements

So when the elements are located? The lookup takes place each time the field is accessed. So the for example, when we execute the code: newTodoInput.sendKeys(todoName + Keys.ENTER); in createTodo() method the actual instruction that is executed is: driver.findElement(By.className('new-todo')).sendKeys(todoName + Keys.ENTER). We can expect that potential exception that element was not found is thrown not during the object initialization but during the first element lookup.

Selenium uses Proxy pattern to achieve described behaviour.

@CacheLookup

There are situations when there is no need to lookup for elements each time the annotated field is accessed. In such a case we can use @CacheLookup annotation. In our example the input field does not change on the page so its lookup can be cached:

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

Running the tests

It is high time to execute the tests. It can be done either from IDE or using the terminal:

1
./gradlew clean test --tests *TodoMvcTests

The build was successful with all tests passed:

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

Next steps

In the next part of this tutorial you will learn how to improve the project configuration. You will learn about executing tests in parallel, tests execution order, parameterized tests, AssertJ and more.

Published on Java Code Geeks with permission by Rafal Borowiec, partner at our JCG program. See the original article here: JUnit 5 and Selenium – Using Selenium built-in `PageFactory` to implement Page Object Pattern

Opinions expressed by Java Code Geeks contributors are their own.

(0 rating, 0 votes)
You need to be a registered member to rate this.
Start the discussion Views Tweet it!
Do you want to know how to develop your skillset to become a Java Rockstar?
Subscribe to our newsletter to start Rocking right now!
To get you started we give you our best selling eBooks for FREE!
1. JPA Mini Book
2. JVM Troubleshooting Guide
3. JUnit Tutorial for Unit Testing
4. Java Annotations Tutorial
5. Java Interview Questions
6. Spring Interview Questions
7. Android UI Design
and many more ....
I agree to the Terms and Privacy Policy

Leave a Reply

avatar

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  Subscribe  
Notify of