Page Object pattern example

twittergoogle_plusrss

Facebooktwittergoogle_plusredditlinkedinmail

This blog post will provide example of end-to-end Selenium tests in Java with usage of Page Object pattern.

Every responsible developer knows that end-to-end test is necessary at some point. When you want to test web application end-to-end, there is only one decent open-source option: Selenium. Of course end-to-end test suite have to be maintainable. To achieve that, I believe we have to use Page Object pattern for each web screen present in web application. Martin Fowler’s link explains very well benefits of this pattern, therefore I skip theory.

We at Dotsub are using Java platform with Spring Boot for back-end. Therefore Java Selenium APIs were natural fit for our needs. This blog post will explain Page Object pattern on application built with Spring Boot. For UI, it will use famous Dan Abramov’s Todos  example. This example was used for Getting started with Redux tutorial. It is simple single page application built on top of React + Redux JavaSript frameworks. It provides us decent functionality we can cover by Selenium test examples.

Functionality for testing

Let’s take a look at functionality of this simple example.

Page Object: todos-all

In text Input field, we can enter name of Todo item. Button Add Todo saves item into Todos list. When we click on any Todo item we can mark it as complete or incomplete.

Page Object: todos-active

With Show buttons below the Todos list, we can filter items to show only Active

Page Object: todos-complete

…or Completed Todo items.

In fact this Todos example application doesn’t communicate with Spring Boot server. Front-end assets are just served from Spring Boot application. But we don’t need such communication to demonstrate Page Object example. I used Spring Boot, because it’s trivial to integrate Selenium tests with it. Note that goal of this blog post isn’t to explain Selenium or its APIs. It is expected for reader to be slightly familiar with them already.

Page Object class

As we mentioned, our end-to-end test will use Page object pattern. Goal of this pattern is to separate HTML details of testing page from actions that can be performed against the page. Listing that shows Page Object class for Todos application will be split into few parts, so that we can explain each part separately:

package net.lkrnac.blog.pageobject.e2e;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.util.List;

import static java.lang.String.format;
import static org.junit.Assert.*;


public class TodoPageObject {
    private WebDriver driver;
    private WebDriverWait wait;

    public TodoPageObject(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, 10);
    }

    public TodoPageObject get() {
        driver.get("localhost:8080");
        wait.until(ExpectedConditions.elementToBeClickable(By.tagName("button")));
        return this;
    }

Page Object class has two fields. First is Selenium WebDriver instance, which represents browser’s window wrapper used for controlling web page similar to real user. WebDriverWait is helper for waiting until HTML elements are rendered. Method get is used to load Todos page based on it’s URL.

    private WebElement findElementWithText(String text) {
        return driver.findElement(getConditionForText(text));
    }

    private List<WebElement> findElementsWithText(String text) {
        return driver.findElements(getConditionForText(text));
    }

    private By getConditionForText(String text) {
        return By.xpath(format("//*[text()='%s']", text));
    }

These three methods are private helpers for Todos actions shown later. We will used them to find HTML element or list of HTML elements based on the text they contain. For text search we use Selenium’s XPath APIs.

    public TodoPageObject addTodo(String todo) {
        WebElement input = driver.findElement(By.tagName("input"));
        input.sendKeys(todo);
        WebElement button = driver.findElement(By.tagName("button"));
        button.click();
        return this;
    }

    public TodoPageObject clickOnTodoItem(String todoItem) {
        findElementWithText(todoItem).click();
        return this;
    }

I believe names addTodo and clickOnTodoItem are self explanatory. We will use them to enter test into Todos input list, click Add Todo button and to mark Todo items complete/incomplete. Notice that return value is Todos Page Object instance, so that we can use fluent API during testing. This mechanism will keep our tests less verbose.

    public TodoPageObject selectAll() {
        findElementWithText("All").click();
        return this;
    }

    public TodoPageObject selectActive() {
        findElementWithText("Active").click();
        return this;
    }

    public TodoPageObject selectCompleted() {
        findElementWithText("Completed").click();
        return this;
    }

These three methods are using for clicking on filter buttons All/Active/Complete.

    public TodoPageObject verifyTodoShown(String todoItem, boolean expectedStrikethrough) {
        WebElement todoElement = findElementWithText(todoItem);
        assertNotNull(todoElement);
        boolean actualStrikethrough = todoElement.getAttribute("style").contains("text-decoration: line-through;");
        assertEquals(expectedStrikethrough, actualStrikethrough);
        return this;
    }

    public TodoPageObject verifyTodoNotShown(String todoItem) {
        assertTrue(findElementsWithText(todoItem).isEmpty());
        return this;
    }
}

Last two methods are used to verify if given Todo item is shown on not shown. When it’s shown we can verify if it’s completed via parameter expectedStrikethrough.

Tests

With such page object support we can start testing:

package net.lkrnac.blog.pageobject.e2e;

import io.github.bonigarcia.wdm.ChromeDriverManager;
import net.lkrnac.blog.pageobject.TodoApplication;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TodoApplication.class)
@WebIntegrationTest
public class TodoAppTest {
    private static WebDriver driver;

    @BeforeClass
    public static void setUp() {
        ChromeDriverManager.getInstance().setup();
        driver = new ChromeDriver();
    }

    @AfterClass
    public static void tearDown() {
        driver.quit();
    }

    @Test
    public void testCreateTodos() {
        // GIVEN
        new TodoPageObject(driver).get()

            // WHEN
            .addTodo("testTodo1")
            .addTodo("testTodo2")

            // THEN
            .verifyTodoShown("testTodo1", false)
            .verifyTodoShown("testTodo2", false);
    }

    @Test
    public void testCompleteTodo() {
        // GIVEN
        new TodoPageObject(driver).get()
            .addTodo("testTodo1")
            .addTodo("testTodo2")

            // WHEN
            .clickOnTodoItem("testTodo1")

            // THEN
            .verifyTodoShown("testTodo1", true)
            .verifyTodoShown("testTodo2", false);
    }

    @Test
    public void testSelectActive() {
        // GIVEN
        new TodoPageObject(driver).get()
            .addTodo("testTodo1")
            .addTodo("testTodo2")
            .clickOnTodoItem("testTodo1")

            // WHEN
            .selectActive()

            // THEN
            .verifyTodoNotShown("testTodo1")
            .verifyTodoShown("testTodo2", false);
    }

    @Test
    public void testSelectCompleted() {
        // GIVEN
        new TodoPageObject(driver).get()
            .addTodo("testTodo1")
            .addTodo("testTodo2")
            .clickOnTodoItem("testTodo1")

            // WHEN
            .selectCompleted()

            // THEN
            .verifyTodoShown("testTodo1", true)
            .verifyTodoNotShown("testTodo2");
    }

    @Test
    public void testSelectAll() {
        // GIVEN
        new TodoPageObject(driver).get()
            .addTodo("testTodo1")
            .addTodo("testTodo2")
            .clickOnTodoItem("testTodo1")
            .selectCompleted()

            // WHEN
            .selectAll()

            // THEN
            .verifyTodoShown("testTodo1", true)
            .verifyTodoShown("testTodo2", false);
    }
}

These test cases cover some basic scenarios real user could perform against our Todos application. I am not going to explain them, because they are nicely readable due to usage of Page Object pattern. Separate huge benefit of this pattern I want to point out is decoupling tests from HTML. If we would change HTML structure on Todos page, we would need to change only Page Object class. Test cases would remain untouched.

Example code with all project files is hosted on Github.

twittergoogle_plusrss