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.
This lesson assumes that:
- You are familiar with Selenium
- You can configure your end-to-end tests
- You know how you can find elements from HTML documents
- You can control a web browser by using Selenium WebDriver
- You know what a page object is
- You understand how you can create first page object
- You can write end-to-end tests for a feature that renders a single item
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
idof the shown task is appended to theidof thetrHTML element that wraps the information of a single task. - The
statusof the shown task is appended to theclassof thetdHTML 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 ofTaskListItemobjects. 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 returnstrueif the task list page is open andfalseotherwise. - The
open()method opens the task list page and returns a newTaskListPageobject.
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 theidof the shown task from theidof the relevanttrHTML element and returns the foundid. - The
getTitle()method returns the title of the shown task. - The
getStatus()method parses thestatusof the shown task from theclassof the relevanttdHTML element and returns the foundstatus. - The
viewTask()method clicks the link that opens the view task page and returns a newTaskPageobject.
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}
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:
- 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.
- Create a new
TaskListPageobject and set a reference to the created object to thetaskListPagefield.
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}
Additional Reading:
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.
Nevertheless, we can write four end-to-end tests for the task list feature:
- We have to verify that we can open the task list page.
- 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.
- 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.
- 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.