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.
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.
With Show
buttons below the Todos list, we can filter items to show only Active
…
…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.