JUnit 5 and Selenium Foundation

Using Selenium's built-in PageFactory to implement page object mode

In this section, the implementation of the Page Object pattern is introduced through Selenium's built-in PageFactory support class. PageFactory provides a mechanism to initialize any Page Object that declares a WebElement or a list < WebElement > field with the @ FindBy annotation.

Due to the indescribable reason, I have packed the test web page. Please pay attention to the information at the end of the article if you need.

Introduction to page object mode

The goal of the page object pattern is to abstract application pages and functions from actual tests. The page object pattern improves code reusability between tests and fixtures, but also makes code easy to maintain.

Page API or page object

We'll start with a project that models TodoMVC pages as page objects. This object represents the page API that will be used in the test. You can use interfaces to model the API itself. If you look at the methods of the following interface, you will notice that they are only user functions available on the page. You can create a to-do, rename it, or delete it:

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 hides all implementation details. In fact, it has nothing to do with Selenium WebDriver. Therefore, in theory, we can use different implementations of this page for different devices, such as mobile native applications, desktop applications, and Web applications.

Create test

After defining the page API, you can directly jump to create a test method. After confirming that the API can be used to create tests, implement the page. This design pattern allows testers to focus on the actual use of the application without falling into the pit of detail too early.

The following tests were created:

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

In the above test class, we see that before each test, ChromeDriver @ BeforeEach has been initialized with the Selenium Jupiter extension (@ extendwith (selenium extension. Class)) and injected into the setting method. The driver object will be used to initialize the page object.

The page object pattern depends largely on the characteristics of the project. You may want to use interfaces often, but this is not necessary. You may want to consider at a lower level of abstraction, where API s are more detailed methods exposed, such as setTodoInput(String value), clickSubmitButton().

Using Selenium's built-in PageFactory to implement Page Object Pattern

We already have an interface to model the behavior of todo MVC pages, and we have failure tests using API. The next step is to actually implement the page object. To do this, we will use the Selenium built-in PageFactory class and its utilities.

The PageFactory class simplifies the implementation of the Page Object pattern. This class provides a mechanism to initialize any Page Object that declares a WebElement or a list < WebElement > field with the @ FindBy annotation. PageFactory provides comments that support the implementation of the Page Object mode and other comments.

The following TodoMvcPage class implements the interface we created earlier. It declares several fields with the @ FindBy annotation. It also declares a constructor that constructs the WebDriver function with the parameters used by the factory to initialize the field:

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("***");
    }
 
    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 comment used to find elements in Page Object. There are @ FindBys and @ FindAll.

@FindBys

@The FindBys annotation is used to tag fields on the Page Object to indicate that the lookup should use a series of @ FindBy tags. In this example, selenium will search for components with class = "button" being internal and component id = "menu":

@FindBys({
  @FindBy(id = "menu"),
  @FindBy(className = "button")
})
private WebElement element;

@FindAll

@The FindAll annotation marks the fields on the Page Object to indicate that the lookup should use a series of @ FindBy tags. In this example, Selenium searches for all element IDS = menu with class = button and. Elements are not guaranteed to be in document order:

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

PageFactory initializes the Page object

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

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

Use reflection to initialize the object in PageFactory, and then initialize all WebElements or list < WebElement > marked with the field @ FindBy annotation. Using this method requires the Page Object to have a single parameter constructor that accepts the WebDriver object.

Location element

So when is the element positioned? Every time you access this field, you find it. For example, when we execute the code: new TodoInput.sendKeys(todoName + Keys.ENTER); in the createtodo() method, the actual executed instruction is: driver. Findelement (by. Classname ('New todo '))). Sendkeys (todoname + keys. Enter). Raises a potential exception for an element not found, not during object initialization but during the first element lookup. Selenium uses the proxy pattern to implement the described behavior.

@CacheLookup

In some cases, you don't need to look up elements every time you access a annotated field. In this case, we can use the @ CacheLookup annotation. In the example, the input field does not change on the page, so you can cache the lookup results:

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

Operation test

Now is the time to perform the test. You can do this from the IDE or using a terminal:

./gradlew clean test --tests *TodoMvcTests

Through all tests, the build was successful:

> Task :test
 
demos.selenium.todomvc.TodoMvcTests > editsTodo() PASSED
 
demos.selenium.todomvc.TodoMvcTests > togglesTodoCompleted() PASSED
 
demos.selenium.todomvc.TodoMvcTests > createsTodo() PASSED
 
demos.selenium.todomvc.TodoMvcTests > removesTodo() PASSED
 
demos.selenium.todomvc.TodoMvcTests > togglesAllTodosCompleted() PASSED
 
demos.selenium.todomvc.TodoMvcTests > createsTodosWithSameName() PASSED
 
demos.selenium.todomvc.TodoMvcTests > clearsCompletedTodos() PASSED
 
BUILD SUCCESSFUL in 27s
3 actionable tasks: 3 executed

The WeChat public's background replied to the test page to get the download address of the test page.

  • Solemnly declare: the article begins with the public number "FunTester", prohibits the third parties (except Tencent cloud) reprint and publish.

Selected technical articles

Selected non-technical articles

Tags: Selenium Java Linux Mobile

Posted on Mon, 13 Jan 2020 23:56:44 -0500 by serial