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:
- Part 1 – Setup the project from the ground up – Gradle with JUnit 5 and Jupiter Selenium
- Part 2 – Using Selenium built-in
PageFactory
to implement Page Object Pattern
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() { } 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. |