Writing End-to-End Tests for Spring Web Applications - Rendering a List

After we have finished this lesson, we

  • Know how we can create page objects for a list page.
  • Understand the challenges that we face if we initialize our database into a known state when our web application is started.
  • Can write end-to-end tests for features that render a list page.

Introduction to System Under Test

During this lesson we will write end-to-end tests for a feature that renders the task list page. This page is rendered when an authenticated user sends a GET request to the path: ‘/’, and the task list page fulfills these business requirements:

  • It must display the number of tasks found from the database.
  • It must display the tasks found from the database.
  • It must display the title and the localized status of each task.
  • It must display a link that opens the view task page.

This page has also one problem. It doesn’t display all information that is required by our our end-to-end tests. That’s why we have to add some hidden information to this page. This information is described in the following:

  • The id of the shown task is appended to the id of the tr HTML element that wraps the information of a single task.
  • The status of the shown task is appended to the class of the td HTML element that displays the localized status of the task.

The source code of our JSP page (list.jsp) looks as follows:

 1<!DOCTYPE html>
 2<%@ page contentType="text/html;charset=UTF-8" language="java" %>
 3<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
 4<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
 5<html>
 6<head>
 7    <title><spring:message code="page.task.list.title"/></title>
 8</head>
 9<body>
10<h1><spring:message code="page.task.list.title"/></h1>
11<div>
12    <spring:message code="page.task.list.size.message"
13                    arguments="${tasks.size()}"/>
14</div>
15<c:if test="${not empty tasks}">
16    <table id="task-list" class="table table-striped">
17        <thead>
18        <tr>
19            <th><spring:message code="task.property.title.header"/></th>
20            <th><spring:message code="task.property.status.header"/></th>
21        </tr>
22        </thead>
23        <tbody>
24        <c:forEach items="${tasks}" var="task">
25            <tr id="task-list-item-${task.id}" class="task-list-item">
26                <td>
27                    <a class="task-list-item-title"
28                       href="${pageContext.request.contextPath}/task/${task.id}">
29                        <c:out value="${task.title}"/>
30                    </a>
31                </td>
32                <td class="task-list-item-status task-status-${task.status}">
33                    <spring:message code="${task.status.localizationKey}"/>
34                </td>
35            </tr>
36        </c:forEach>
37        </tbody>
38    </table>
39</c:if>
40</body>
41</html>

Let’s move on and create the required page objects.

Creating the Required Page Objects

Before we can write end-to-end tests for the task list feature, we have to create two page objects. These page objects are:

First, the TaskListPage class is page object that represents the task list page. It has four methods that are described in the following:

  • The getListItems() method returns a list of TaskListItem objects. In other words, this method returns the page objects which represent the tasks shown on the task list page.
  • The getPageUrl() method returns the URL of the task list page.
  • The isOpen() method returns true if the task list page is open and false otherwise.
  • The open() method opens the task list page and returns a new TaskListPage object.

The source code of the TaskListPage class looks as follows:

 1import com.testwithspring.intermediate.WebDriverUrlBuilder;
 2import org.openqa.selenium.By;
 3import org.openqa.selenium.WebDriver;
 4
 5import java.util.ArrayList;
 6import java.util.List;
 7
 8public final class TaskListPage {
 9
10    private final WebDriver browser;
11    private final String pageUrl;
12
13    public TaskListPage(WebDriver browser) {
14        this.browser = browser;
15        this.pageUrl = WebDriverUrlBuilder.buildFromPath("/");
16    }
17
18    List<TaskListItem> getListItems() {
19        List<TaskListItem> listItems = new ArrayList<>();
20        browser.findElements(By.className("task-list-item"))
21                .forEach(e -> listItems.add(new TaskListItem(browser, e)));
22        return listItems;
23    }
24
25    String getPageUrl() {
26        return pageUrl;
27    }
28
29    public boolean isOpen() {
30        return browser.getCurrentUrl().equals(pageUrl);
31    }
32
33    public TaskListPage open() {
34        browser.get(pageUrl);
35        return new TaskListPage(browser);
36    }
37}

Second, the TaskListItem class is page object which represents a task that is shown on the task list page. It’s a good idea to create a separate page object for a list item because it allows us to separate the responsibilities of our page objects.

This class has four methods that are described in the following:

  • The getId() method parses the id of the shown task from the id of the relevant tr HTML element and returns the found id.
  • The getTitle() method returns the title of the shown task.
  • The getStatus() method parses the status of the shown task from the class of the relevant td HTML element and returns the found status.
  • The viewTask() method clicks the link that opens the view task page and returns a new TaskPage object.

The source code of the TaskListItem class looks as follows:

 1import org.openqa.selenium.By;
 2import org.openqa.selenium.WebDriver;
 3import org.openqa.selenium.WebElement;
 4import org.openqa.selenium.support.ui.ExpectedConditions;
 5import org.openqa.selenium.support.ui.WebDriverWait;
 6
 7final class TaskListItem {
 8
 9    private final WebDriver browser;
10    private final WebElement itemRootElement;
11
12    TaskListItem(WebDriver browser, WebElement itemRootElement) {
13        this.browser = browser;
14        this.itemRootElement = itemRootElement;
15    }
16
17    Long getId() {
18        String elementId = itemRootElement.getAttribute("id");
19        String taskId = elementId.replace("task-list-item-", "");
20        return Long.parseLong(taskId);
21    }
22
23    String getTitle() {
24        return itemRootElement.findElement(
25                By.className("task-list-item-title")
26        )
27                .getText();
28    }
29
30    TaskStatus getStatus() {
31        WebElement statusElement = itemRootElement.findElement(
32                By.className("task-list-item-status")
33        );
34
35        String status = statusElement.getAttribute("class")
36                .replace("task-list-item-status", "")
37                .trim()
38                .replace("task-status-", "");
39
40        return TaskStatus.valueOf(status);
41    }
42
43    TaskPage viewTask() {
44        WebElement viewTaskLink = itemRootElement.findElement(
45                By.className("task-list-item-title")
46        );
47        TaskPage targetPage = new TaskPage(browser, getId());
48
49        viewTaskLink.click();
50
51        WebDriverWait wait = new WebDriverWait(browser, 3);
52        wait.until(ExpectedConditions.urlToBe(targetPage.getPageUrl()));
53
54        return targetPage;
55    }
56}
Our end-to-end tests use a few other page objects as well. These page objects were described in the previous parts of this topic, and that is why I won’t repeat myself here.

Next, we will find out how we can use these page objects when we write end-to-end tests for the task list feature.

Writing End-to-End Tests for the Task List Feature

We can write our end-to-end tests by following these steps:

First, we have to create a new test class and configure Selenium WebDriver. After we have created our test class, its source code looks as follows:

 1import org.junit.experimental.categories.Category;
 2import org.junit.runner.RunWith;
 3import org.openqa.selenium.WebDriver;
 4import org.openqa.selenium.chrome.ChromeDriver;
 5
 6@RunWith(SeleniumTestRunner.class)
 7@SeleniumTest(driver = ChromeDriver.class)
 8@Category(EndToEndTest.class)
 9public class RenderTaskListPageTest {
10
11    @SeleniumWebDriver
12    private WebDriver browser;
13}

Second, we have to add a TaskListPage field to our test class. Our test methods will use this field when they open the task list page.

After we have added this field into our test class, its source code looks as follows:

 1import org.junit.experimental.categories.Category;
 2import org.junit.runner.RunWith;
 3import org.openqa.selenium.WebDriver;
 4import org.openqa.selenium.chrome.ChromeDriver;
 5
 6@RunWith(SeleniumTestRunner.class)
 7@SeleniumTest(driver = ChromeDriver.class)
 8@Category(EndToEndTest.class)
 9public class RenderTaskListPageTest {
10
11    @SeleniumWebDriver
12    private WebDriver browser;
13
14    private TaskListPage taskListPage;
15}

Third, we have to add a setup method to our test class and implement the setup method by following these steps:

  1. Log the user in before a test method is run. We have to do this because our web application doesn’t allow anonymous users to access the information that is found from the database.
  2. Create a new TaskListPage object and set a reference to the created object to the taskListPage field.

After we have written our setup method, the source code of our test class looks as follows:

 1import org.junit.Before;
 2import org.junit.experimental.categories.Category;
 3import org.junit.runner.RunWith;
 4import org.openqa.selenium.WebDriver;
 5import org.openqa.selenium.chrome.ChromeDriver;
 6
 7@RunWith(SeleniumTestRunner.class)
 8@SeleniumTest(driver = ChromeDriver.class)
 9@Category(EndToEndTest.class)
10public class RenderTaskListPageTest {
11
12    @SeleniumWebDriver
13    private WebDriver browser;
14
15    private TaskListPage taskListPage;
16
17    @Before
18    public void configureTestCases() {
19        logUserIn();
20        taskListPage = new TaskListPage(browser);
21    }
22
23    private void logUserIn() {
24        LoginPage loginPage = new LoginPage(browser).open();
25        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD);
26    }
27}

As we can see, our setup method uses the constant class called: EndToEndTestUsers that contains the users of our web application. The relevant part of the EndToEndTestUsers class looks as follows:

 1public final class EndToEndTestUsers {
 2
 3    private EndToEndTestUsers() {}
 4
 5    public static class JohnDoe {
 6
 7        public static final String EMAIL_ADDRESS = "john.doe@gmail.com";
 8        public static final String PASSWORD = "user";
 9    }
10}

Fourth, we have to ensure that the logged in user is logged out after a test method has been run. This is a very important step because it helps us to verify that the correct user is logged in when an end-to-end test is run.

After we have written the required teardown method, the source code of our test class looks as follows:

 1import org.junit.After;
 2import org.junit.Before;
 3import org.junit.experimental.categories.Category;
 4import org.junit.runner.RunWith;
 5import org.openqa.selenium.WebDriver;
 6import org.openqa.selenium.chrome.ChromeDriver;
 7
 8@RunWith(SeleniumTestRunner.class)
 9@SeleniumTest(driver = ChromeDriver.class)
10@Category(EndToEndTest.class)
11public class RenderTaskListPageTest {
12
13    @SeleniumWebDriver
14    private WebDriver browser;
15
16    private TaskListPage taskListPage;
17
18    @Before
19    public void configureTestCases() {
20        logUserIn();
21        taskListPage = new TaskListPage(browser);
22    }
23
24    private void logUserIn() {
25        LoginPage loginPage = new LoginPage(browser).open();
26        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD);
27    }
28
29    @After
30    public void logUserOut() {
31        new NavigationBar(browser).logUserOut();
32    }
33}

Fifth, we have to write end-to-end tests for the task list feature. If we are initializing our database into a known state when our web application is started, writing these end-to-end tests is a bit tricky because we cannot know the order in which our end-to-end tests are run.

If we are writing end-to-end tests for a Spring Boot web application, we don’t have this problem because we can initialize our database into a known state before an end-to-end test is run.

Nevertheless, we can write four end-to-end tests for the task list feature:

  1. We have to verify that we can open the task list page.
  2. We have to ensure that the task list page displays at least two tasks. Because our SQL script contains three tasks and we want to write end-to-end tests for features that delete existing tasks and create new tasks, our database contains at least two tasks when this end-to-end test is run.
  3. We have to verify that the task list page displays the correct information of the first task. Again, this test seems a bit weird. However, because we use the second task found from our SQL script when we write end-to-end tests for the update task feature, we don’t know what its field values are when this end-to-end test is run. That’s why we can assert only the field values of the first task.
  4. We have to ensure that we can navigate from the task list page to the view task page.

After we have written these end-to-end tests, the source code of our test class looks as follows:

 1import org.junit.After;
 2import org.junit.Before;
 3import org.junit.Test;
 4import org.junit.experimental.categories.Category;
 5import org.junit.runner.RunWith;
 6import org.openqa.selenium.WebDriver;
 7import org.openqa.selenium.chrome.ChromeDriver;
 8
 9import java.util.List;
10
11import static org.assertj.core.api.Assertions.assertThat;
12
13@RunWith(SeleniumTestRunner.class)
14@SeleniumTest(driver = ChromeDriver.class)
15@Category(EndToEndTest.class)
16public class RenderTaskListPageTest {
17
18    @SeleniumWebDriver
19    private WebDriver browser;
20
21    private TaskListPage taskListPage;
22
23    @Before
24    public void configureTestCases() {
25        logUserIn();
26        taskListPage = new TaskListPage(browser);
27    }
28
29    private void logUserIn() {
30        LoginPage loginPage = new LoginPage(browser).open();
31        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD);
32    }
33
34    @Test
35    public void shouldOpenTaskListPage() {
36        TaskListPage shown = taskListPage.open();
37        assertThat(shown.isOpen()).isTrue();
38    }
39
40    @Test
41    public void shouldShowAtLeastTwoTasks() {
42        TaskListPage shown = taskListPage.open();
43        List<TaskListItem> tasks = shown.getListItems();
44        assertThat(tasks.size()).isGreaterThanOrEqualTo(2);
45    }
46
47    @Test
48    public void shouldShowCorrectInformationOfFirstTask() {
49        TaskListPage shown = taskListPage.open();
50        TaskListItem first = shown.getListItems().get(0);
51
52        assertThat(first.getId())
53                .isEqualByComparingTo(EndToEndTestTasks.WriteExampleApp.ID);
54        assertThat(first.getTitle())
55                .isEqualTo(EndToEndTestTasks.WriteExampleApp.TITLE);
56        assertThat(first.getStatus())
57                .isEqualTo(EndToEndTestTasks.WriteExampleApp.STATUS);
58    }
59
60    @Test
61    public void shouldAllowUsToNavigateToViewTaskPage() {
62        TaskListPage taskListPage = this.taskListPage.open();
63        TaskListItem first = taskListPage.getListItems().get(0);
64
65        TaskPage shown = first.viewTask();
66        assertThat(shown.isOpen()).isTrue();
67    }
68
69    @After
70    public void logUserOut() {
71        new NavigationBar(browser).logUserOut();
72    }
73}

Additional Reading:

As we can see, our end-to-end test uses the constant class called: EndToEndTestTasks which contains the tasks that are inserted into our database. The relevant part of the EndToEndTestTasks class looks as follows:

 1public final class EndToEndTestTasks {
 2
 3    private EndToEndTestTasks() {}
 4
 5    public static class WriteExampleApp {
 6
 7        public static class Creator {
 8
 9            public static final Long ID = 1L;
10            public static final String NAME = "John Doe";
11        }
12
13        public static final Long ID = 1L;
14        public static final String TITLE = "Write example application";
15    }
16}

We have now written end-to-end tests for the task list feature. Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us three things:

  • If the information that is required by our end-to-end tests is not visible on the user interface, we can add hidden information to the rendered page.
  • It’s a good idea to create a page object that represents a single list item. This helps us to separate the responsibilities of our page objects.
  • If we initialize our database into a known state when our web application is started, we cannot write comprehensive end-to-end tests for a list page feature because we cannot know how many items are found from the database when our end-to-end tests are run.

Get the source code from Github