Writing End-to-End Tests for Spring Web Applications - Rendering a Single Item

After we have finished this lesson, we:

  • Know how we can log a user in before a test method is run.
  • Can write end-to-end tests that use page objects.
  • Know how we can log a user out after a test method has been run.

Introduction to System Under Test

During this lesson we will write end-to-end tests for a controller method that renders the view task page. This page fulfills these requirements:

  • The view task page is rendered when an authenticated user sends a GET request to the path: ‘/task/{taskId}’.
  • This page must display the title and description of the task.
  • If the task is assigned to someone, this page must display the name of the assignee.
  • The view task page must display the creation time and the name of the person who created the shown task.
  • This page must display the modification time and the name of the person who made the last changes to the information of the shown task.
  • If the task is closed, this page must display the closing time and the name of person who closed the task.
  • This page must display the links that are used to delete the task and open the update task page.

The source code of our JSP page (view.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="form" uri="http://www.springframework.org/tags/form" %>
 5<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
 6<%@ taglib uri="http://sargue.net/jsptags/time" prefix="javatime" %>
 7<html>
 8<head>
 9    <title></title>
10</head>
11<body>
12<h1 id="task-title"><c:out value="${task.title}"/></h1>
13<div class="panel panel-default">
14    <div class="panel-body">
15        <div id="task-description">
16            <c:choose>
17                <c:when test="${empty task.description}">
18                    <spring:message code="page.view.task.no.description.message"/>
19                </c:when>
20                <c:otherwise>
21                    <c:out value="${task.description}"/>
22                </c:otherwise>
23            </c:choose>
24        </div>
25    </div>
26</div>
27<div id="lifecycle-fields" class="container-fluid">
28    <c:if test="${task.assignee != null}">
29        <div class="row">
30            <div class="col-md-3">
31                <spring:message code="page.view.task.assignee.description"/>:
32                <span id="assignee-name">
33                    <c:out value="${task.assignee.name}"/>
34                </span>
35            </div>
36        </div>
37    </c:if>
38    <div class="row">
39        <div class="col-md-3">
40            <spring:message code="page.view.task.creator.description.prefix"/>
41            <span id="creator-name"><c:out value="${task.creator.name}"/></span>
42            <spring:message code="page.view.task.timestamp.prefix"/>
43            <span id="creation-time">
44                <javatime:format value="${task.creationTime}"
45                                 pattern="yyyy-MM-dd H:mm:ss"/>
46            </span>
47        </div>
48        <div class="col-md-3">
49            <spring:message code="page.view.task.modifier.description.prefix"/>
50            <span id="modifier-name">
51                <c:out value="${task.modifier.name}"/>
52            </span>
53            <spring:message code="page.view.task.timestamp.prefix"/>
54            <span id="modification-time">
55                <javatime:format value="${task.modificationTime}"
56                                 pattern="yyyy-MM-dd H:mm:ss"/>
57            </span>
58        </div>
59        <c:if test="${task.status == 'CLOSED'}">
60            <div id="closed-task-fields" class="col-md-3">
61                <spring:message code="page.view.task.closer.description.prefix"/>
62                <span id="closer-name">
63                    <c:out value="${task.closer.name}"/
64                </span>
65                <spring:message code="page.view.task.timestamp.prefix"/>
66                <span id="closing-time">
67                    <javatime:format value="${task.modificationTime}"
68                                     pattern="yyyy-MM-dd H:mm:ss"/>
69                </span>
70            </div>
71        </c:if>
72    </div>
73    <div id="task-action-links" class="row">
74        <div class="col-md-1">
75            <a id="delete-task-link"
76               href="${pageContext.request.contextPath}/task/${task.id}/delete">
77                <spring:message code="page.view.task.delete.task.link.label"/>
78            </a>
79        </div>
80        <div class="col-md-1">
81            <a id="edit-task-link"
82               href="${pageContext.request.contextPath}/task/${task.id}/update">
83                <spring:message code="page.view.task.edit.task.link.label"/>
84            </a>
85        </div>
86    </div>
87</div>
88</body>
89</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 view task feature, we have to create five page objects. Let’s go through these objects one by one.

First, the NavigationBar class is a page object that represents the navigation bar which is visible on every page of our web application. It has one method called: logUserOut() that logs the current user out of our web application. Our end-to-end tests will invoke this method after a test method has been run.

The source code of the NavigationBar class looks as follows:

 1import com.testwithspring.intermediate.web.login.LoginPage;
 2import org.openqa.selenium.By;
 3import org.openqa.selenium.WebDriver;
 4
 5public final class NavigationBar {
 6
 7    private final WebDriver browser;
 8
 9    public NavigationBar(WebDriver browser) {
10        this.browser = browser;
11    }
12
13    public LoginPage logUserOut() {
14        if (browser.findElements(By.id("logout-form")).isEmpty()) {
15            throw new RuntimeException("The logout button is not visible");
16        }
17
18        browser.findElement(By.id("logout-form")).submit();
19        return new LoginPage(browser);
20    }
21}

Second, the TaskPage class is a page object that represents the view task page. This class provides several different methods that are described in the following:

  • It provides two methods that return other page objects. These methods allow us to access the other sections of the view task page.
  • It provides two methods that return the title and description of the shown task.
  • The open() method opens the view task page.
  • The isOpen() method returns true if the view task page is open and false otherwise.

The source code of the TaskPage class looks as follows:

 1import org.openqa.selenium.By;
 2import org.openqa.selenium.WebDriver;
 3
 4final class TaskPage {
 5
 6    private final WebDriver browser;
 7    private final String pageUrl;
 8    private final Long taskId;
 9
10    TaskPage(WebDriver browser, Long taskId) {
11        this.browser = browser;
12        this.pageUrl = WebDriverUrlBuilder.buildFromPath("/task/%d", taskId);
13        this.taskId = taskId;
14    }
15
16    TaskActions getTaskActions() {
17        return new TaskActions(browser,
18                browser.findElement(By.id("task-action-links")),
19                taskId
20        );
21    }
22
23    TaskLifeCycleFields getTaskLifeCycleFields() {
24        return new TaskLifeCycleFields(
25                browser.findElement(By.id("lifecycle-fields"))
26        );
27    }
28
29    String getTaskDescription() {
30        return browser.findElement(By.id("task-description")).getText();
31    }
32
33    String getTaskTitle() {
34        return browser.findElement(By.id("task-title")).getText();
35    }
36
37    TaskPage open() {
38        browser.get(pageUrl);
39        return new TaskPage(browser, taskId);
40    }
41
42    boolean isOpen() {
43        return browser.getCurrentUrl().equals(pageUrl);
44    }
45}

Third, the TaskLifeCycleFields class is page object that allows us to access the life cycle information of the shown task. We can use this class when we want to ensure that the life cycle information of the shown task is rendered correctly.

The source code of the TaskLifeCycleFields class looks as follows:

 1import org.openqa.selenium.By;
 2import org.openqa.selenium.WebElement;
 3
 4final class TaskLifeCycleFields {
 5
 6    private final WebElement rootElement;
 7
 8    TaskLifeCycleFields(WebElement rootElement) {
 9        this.rootElement = rootElement;
10    }
11
12    String getAssigneeName() {
13        return rootElement.findElement(By.id("assignee-name")).getText();
14    }
15
16    String getCloserName() {
17        return rootElement.findElement(By.id("closer-name")).getText();
18    }
19
20    String getCreatorName() {
21        return rootElement.findElement(By.id("creator-name")).getText();
22    }
23
24    String getModifierName() {
25        return rootElement.findElement(By.id("modifier-name")).getText();
26    }
27
28    boolean isAssigneeNameVisible() {
29        return rootElement.findElements(By.id("assignee-name")).size() == 1;
30    }
31
32    boolean areClosedTaskFieldsVisible() {
33        return rootElement.findElements(By.id("closed-task-fields"))
34                .size() == 1;
35    }
36}
This class doesn’t provide access to the timestamps shown on the view task page. The reason for this is that we cannot write assertions for these values because our SQL script uses the current date and time when it inserts new rows into the tasks table.

Fourth, the TaskActions class is page object that allows us the click the links found from the view task page. The view task page contains two links that are described in the following:

  • The delete link deletes the shown task from the database.
  • The update link opens the update task page.

The source code of the TaskActions 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 TaskActions {
 8
 9    private final WebDriver browser;
10    private final WebElement rootElement;
11    private final Long taskId;
12
13    TaskActions(WebDriver browser, WebElement rootElement, Long taskId) {
14        this.browser = browser;
15        this.rootElement = rootElement;
16        this.taskId = taskId;
17    }
18
19    TaskListPage deleteTask() {
20        WebElement deleteLink = rootElement.findElement(
21                By.id("delete-task-link")
22        );
23        deleteLink.click();
24
25        TaskListPage targetPage = new TaskListPage(browser);
26
27        WebDriverWait wait = new WebDriverWait(browser, 3);
28        wait.until(ExpectedConditions.urlToBe(targetPage.getPageUrl()));
29
30        return targetPage;
31    }
32
33    UpdateTaskPage updateTask() {
34        WebElement updateTask = rootElement.findElement(
35                By.id("update-task-link")
36        );
37        updateTask.click();
38
39        UpdateTaskPage targetPage = new UpdateTaskPage(browser, taskId);
40
41        WebDriverWait wait = new WebDriverWait(browser, 3);
42        wait.until(ExpectedConditions.urlToBe(targetPage.getPageUrl()));
43
44        return targetPage;
45    }
46}

Fifth, the UpdateTaskPage is a page object that represents the update task page. This class has two methods:

  • The getPageUrl() method returns the URL of the update task page. This method is used by the TaskActions class when it waits that the update task page is opened after we click the update task link.
  • The isOpen() method returns true if the update task page is open and false otherwise.

The source code of the UpdateTaskPage class looks as follows:

 1import org.openqa.selenium.WebDriver;
 2
 3final class UpdateTaskPage {
 4
 5    private final WebDriver browser;
 6    private final String pageUrl;
 7    private final Long taskId;
 8
 9    UpdateTaskPage(WebDriver browser, Long taskId) {
10        this.browser = browser;
11        this.pageUrl = WebDriverUrlBuilder.buildFromPath("/task/%d/update",
12                taskId
13        );
14        this.taskId = taskId;
15    }
16
17    String getPageUrl() {
18        return pageUrl;
19    }
20
21    boolean isOpen() {
22        return browser.getCurrentUrl().equals(pageUrl);
23    }
24}

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

Writing End-to-End Tests for the View Task 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 RenderViewTaskPageWhenTaskIsClosedTest {
10
11    @SeleniumWebDriver
12    private WebDriver browser;
13}

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

After we have added this field into our test class, its source code 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 RenderViewTaskPageWhenTaskIsClosedTest {
11
12    @SeleniumWebDriver
13    private WebDriver browser;
14
15    private TaskPage taskPage;
16}

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 TaskPage object and set a reference to the created object to the taskPage 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 RenderViewTaskPageWhenTaskIsClosedTest {
11
12    @SeleniumWebDriver
13    private WebDriver browser;
14
15    private TaskPage taskPage;
16
17    @Before
18    public void configureTestCases() {
19        logUserIn();
20        taskPage = new TaskPage(browser,
21                EndToEndTestTasks.WriteExampleApp.ID
22        );
23    }
24
25    private void logUserIn() {
26        LoginPage loginPage = new LoginPage(browser).open();
27        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD);
28    }
29}

As we can see, our end-to-end tests use constant classes that contain the test data that is inserted into our database. These constant classes are:

The EndToEndTestUsers class 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}

The EndToEndTestTasks class 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}

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 RenderViewTaskPageWhenTaskIsClosedTest {
12
13    @SeleniumWebDriver
14    private WebDriver browser;
15
16    private TaskPage taskPage;
17
18    @Before
19    public void configureTestCases() {
20        logUserIn();
21        taskPage = new TaskPage(browser,
22                EndToEndTestTasks.WriteExampleApp.ID
23        );
24    }
25
26    private void logUserIn() {
27        LoginPage loginPage = new LoginPage(browser).open();
28        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD);
29    }
30
31    @After
32    public void logUserOut() {
33        new NavigationBar(browser).logUserOut();
34    }
35}

Fifth, we have to write four end-to-end tests for the view task feature. These tests are:

  1. Verify that we can open the view task page.
  2. Ensure that the view task page displays the title of the shown task.
  3. Verify that the view task page displays the name of the person who created the task.
  4. Ensure that the update task link is working as expected.

After we have written our 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 static org.assertj.core.api.Assertions.assertThat;
10
11@RunWith(SeleniumTestRunner.class)
12@SeleniumTest(driver = ChromeDriver.class)
13@Category(EndToEndTest.class)
14public class RenderViewTaskPageWhenTaskIsClosedTest {
15
16    @SeleniumWebDriver
17    private WebDriver browser;
18
19    private TaskPage taskPage;
20
21    @Before
22    public void configureTestCases() {
23        logUserIn();
24        taskPage = new TaskPage(browser,
25                EndToEndTestTasks.WriteExampleApp.ID
26        );
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 shouldOpenViewTaskPage() {
36        TaskPage shown = taskPage.open();
37        assertThat(shown.isOpen()).isTrue();
38    }
39
40    @Test
41    public void shouldShowTitleOfViewedTask() {
42        TaskPage shown = taskPage.open();
43        assertThat(shown.getTaskTitle())
44                .isEqualTo(EndToEndTestTasks.WriteExampleApp.TITLE);
45    }
46
47    @Test
48    public void shouldShowCreatorNameOfViewedTask() {
49        TaskPage shown = taskPage.open();
50        TaskLifeCycleFields lifeCycleFields = shown.getTaskLifeCycleFields();
51
52        assertThat(lifeCycleFields.getCreatorName())
53                .isEqualTo(EndToEndTestTasks.WriteExampleApp.Creator.NAME);
54    }
55
56    @Test
57    public void shouldAllowUsToNavigateToUpdateTaskPage() {
58        TaskPage shown = taskPage.open();
59        TaskActions actions = shown.getTaskActions();
60
61        UpdateTaskPage targetPage = actions.updateTask();
62        assertThat(targetPage.isOpen()).isTrue();
63    }
64
65    @After
66    public void logUserOut() {
67        new NavigationBar(browser).logUserOut();
68    }
69}
The real test class has other end-to-end tests as well. If you want to take a look at these end-to-end tests, you can get the example application from Github .

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

Summary

This lesson has taught us three things:

  • If our web application doesn’t allow anonymous users to access the tested feature, we have to log the user in before a test method is run.
  • If we don’t use the WebDriver API in our test classes, we can write end-to-end tests that highlight the requirements of the tested feature.
  • We must log the current user out after a test method has been run because this ensures that every end-to-end test is run by using the correct user.

Get the source code from Github