Writing End-to-End Tests for Spring Web Applications – Submitting a Form

After we have finished this lesson, we:

  • Know how we can create page objects for a page that displays a form.
  • 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 submit a form.

Introduction to System Under Test

During this lesson we will write end-to-end tests for a feature that allows us to create new tasks. The create task page is rendered when an authenticated user sends a GET request to the path: ‘/task/create’, and this page contains a simple form that allows us to specify the title and description of the created task.

The source code of our JSP page (create.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<html>
 7<head>
 8    <title></title>
 9    <script type="text/javascript"
10            src="${pageContext.request.contextPath}/static/js/app/form.js"></script>
11</head>
12<body>
13<h1><spring:message code="page.create.task.title"/></h1>
14<div class="panel panel-default">
15    <div class="panel-body">
16        <form:form id="create-task-form"
17                   action="${pageContext.request.contextPath}/task/create"
18                   method="POST"
19                   modelAttribute="task"
20                   enctype="utf8"
21                   role="form">
22            <input type="hidden"
23                   name="${_csrf.parameterName}"
24                   value="${_csrf.token}"/>
25            <div class="form-group">
26                <label for="title">
27                    <spring:message code="task.title.field.label"/>
28                </label>
29                <form:input id="task-title"
30                            cssClass="form-control"
31                            path="title"/>
32                <form:errors id="error-title"
33                             cssClass="help-block"
34                             path="title"/>
35            </div>
36            <div class="form-group">
37                <label for="description">
38                    <spring:message code="task.description.textarea.label"/>
39                </label>
40                <form:textarea id="task-description"
41                               cssClass="form-control"
42                               path="description"/>
43                <form:errors id="error-description"
44                             cssClass="help-block"
45                             path="description"/>
46            </div>
47            <button type="submit" class="btn btn-default">
48                <spring:message code="page.create.task.form.submit.button.label"/>
49            </button>
50        </form:form>
51    </div>
52</div>
53</body>
54</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 create task feature, we have to create two page objects. These page objects are:

First, the CreateTaskPage class is a page object that represents the create task page. It has three methods that are described in the following:

  1. The getForm() method returns a page object that represents either the create or update task form.
  2. The isOpen() method returns true if the create task page is open and false otherwise.
  3. The open() method opens the create task page and returns a new CreateTaskPage object.

The source code of the CreateTaskPage class looks as follows:

 1import org.openqa.selenium.WebDriver;
 2
 3final class CreateTaskPage {
 4
 5    private final WebDriver browser;
 6    private final String pageUrl;
 7
 8    CreateTaskPage(WebDriver browser) {
 9        this.browser = browser;
10        this.pageUrl = WebDriverUrlBuilder.buildFromPath("/task/create");
11    }
12
13    TaskForm getForm() {
14        return new TaskForm(browser, "create-task-form");
15    }
16
17    boolean isOpen() {
18        return browser.getCurrentUrl().equals(pageUrl);
19    }
20
21    CreateTaskPage open() {
22        browser.get(pageUrl);
23        return new CreateTaskPage(browser);
24    }
25}

Second, the TaskForm class is a page object that represents either the create or update task form. It’s a good idea to create a separate page object for a form because this allows us to separate the responsibilities of our page objects. The API of the TaskForm class has five methods that are described in the following:

  1. The typeDescription() method clears the existing description and sets the value of the description text area.
  2. The typeTitle() method clears the existing title and sets the value of the title text field.
  3. The submitTaskForm() method submits the form and returns a new TaskPage object. If the form in question is the create task form, this method will pass null task id to the constructor of the TaskPage class when it creates a new TaskPage object.
  4. The getTaskDescription() method returns the description of the task.
  5. The getTaskTitle() method returns the title of the task.

The source code of the TaskForm class looks as follows:

 1import org.openqa.selenium.By;
 2import org.openqa.selenium.WebDriver;
 3import org.openqa.selenium.WebElement;
 4
 5import java.util.List;
 6
 7final class TaskForm {
 8
 9    private WebDriver browser;
10    private WebElement form;
11
12    TaskForm(WebDriver browser, String formId) {
13        this.browser = browser;
14        this.form = this.browser.findElement(By.id(formId));
15    }
16
17    void typeDescription(String description) {
18        WebElement descriptionTextArea = form.findElement(
19                By.id("task-description")
20        );
21        descriptionTextArea.clear();
22        descriptionTextArea.sendKeys(description);
23    }
24
25    void typeTitle(String title) {
26        WebElement titleInputField = form.findElement(By.id("task-title"));
27        titleInputField.clear();
28        titleInputField.sendKeys(title);
29    }
30
31    TaskPage submitTaskForm() {
32        Long taskId = getTaskId();
33        form.submit();
34        return new TaskPage(browser, taskId);
35    }
36
37    String getTaskDescription() {
38        return form.findElement(By.id("task-description"))
39                .getAttribute("value");
40    }
41
42    private Long getTaskId() {
43        List<WebElement> idElements = form.findElements(By.id("task-id"));
44        if (idElements.size() == 1) {
45            String taskId = idElements.get(0).getAttribute("value");
46            return Long.parseLong(taskId);
47        }
48        return null;
49    }
50
51    String getTaskTitle() {
52        return form.findElement(By.id("task-title"))
53                .getAttribute("value");
54    }
55}

When we submit the create task form by using valid information, our web application forwards the user to the view task page. That’s why the submitTaskForm() method of the TaskForm class returns a new TaskPage object.

However, if we initialize our database into a known state when our web application is started, we cannot know the id of the created task. This means that we have to pass null task id to the constructor of the TaskPage class when we create a new TaskPage object.

That’s why we have to make two changes to the TaskPage class:

  1. We have to ensure that the open() method throws an exception if the value of the taskId field is null.
  2. We have to add the isOpenWithUnknownTaskId() method to the TaskPage class. This method ignores the task id found from the URL of the view task page, and returns true if the view task page is open and false otherwise.

After we have made these changes, the source code of the TaskPage class looks as follows:

 1import org.apache.commons.lang3.StringUtils;
 2import org.openqa.selenium.By;
 3import org.openqa.selenium.WebDriver;
 4
 5final class TaskPage {
 6
 7    private final WebDriver browser;
 8    private final String pageUrl;
 9    private final Long taskId;
10
11    TaskPage(WebDriver browser, Long taskId) {
12        this.browser = browser;
13        this.pageUrl = WebDriverUrlBuilder.buildFromPath("/task/%d", taskId);
14        this.taskId = taskId;
15    }
16
17    TaskActions getTaskActions() {
18        return new TaskActions(browser,
19                browser.findElement(By.id("task-action-links")),
20                taskId
21        );
22    }
23
24    TaskLifeCycleFields getTaskLifeCycleFields() {
25        return new TaskLifeCycleFields(
26                browser.findElement(By.id("lifecycle-fields"))
27        );
28    }
29
30    String getPageUrl() {
31        return pageUrl;
32    }
33
34    String getTaskDescription() {
35        return browser.findElement(By.id("task-description")).getText();
36    }
37
38    String getTaskTitle() {
39        return browser.findElement(By.id("task-title")).getText();
40    }
41
42    TaskPage open() {
43        if (taskId == null) {
44            throw new RuntimeException("Cannot open view task page");
45        }
46
47        browser.get(pageUrl);
48        return new TaskPage(browser, taskId);
49    }
50
51    boolean isOpen() {
52        return browser.getCurrentUrl().equals(pageUrl);
53    }
54
55    boolean isOpenWithUnknownTaskId() {
56        String pageUrlWithoutId = WebDriverUrlBuilder.buildFromPath("/task/");
57        String taskId = browser.getCurrentUrl()
58                .replace(pageUrlWithoutId, "");
59
60        return StringUtils.isNumeric(taskId);
61    }
62}

If we are writing end-to-end tests for a Spring Boot web application and we initialize our database into a known state before an end-to-end test is run, we don’t have to make these changes because we know the id of the created task.

Also, 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 create task feature.

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

Second, we have to add two constants to our test class. These constants contain the title and description of the created task. Also, we add these constants directly to our test class because they aren’t used by other test classes.

After we have added these constants to 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 SubmitCreateTaskFormWithValidInformationTest {
10
11    private static final String DESCRIPTION = "This is a new lesson";
12    private static final String TITLE = "Write an extra lesson";
13
14    @SeleniumWebDriver
15    private WebDriver browser;
16}

Third, we have to add a CreateTaskPage field to our test class. Our test methods will use this field when they submit the create task form.

After we have added this field to 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 SubmitCreateTaskFormWithValidInformationTest {
10
11    private static final String DESCRIPTION = "This is a new lesson";
12    private static final String TITLE = "Write an extra lesson";
13
14    @SeleniumWebDriver
15    private WebDriver browser;
16
17    private CreateTaskPage createTaskPage;
18}

Fourth, 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 insert new data into the database.
  2. Create a new CreateTaskPage object and open the create task page. Set a reference to the returned CreateTaskPage object to the createTaskPage 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 SubmitCreateTaskFormWithValidInformationTest {
11
12    private static final String DESCRIPTION = "This is a new lesson";
13    private static final String TITLE = "Write an extra lesson";
14
15    @SeleniumWebDriver
16    private WebDriver browser;
17
18    private CreateTaskPage createTaskPage;
19
20    @Before
21    public void configureTestCases() {
22        logUserIn();
23        createTaskPage = new CreateTaskPage(browser).open();
24    }
25
26    private void logUserIn() {
27        LoginPage loginPage = new LoginPage(browser).open();
28        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD);
29    }
30}

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 NAME = "John Doe";
 9        public static final String PASSWORD = "user";
10    }
11}

Fifth, 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
 8import static com.testwithspring.intermediate.EndToEndTestUsers.*;
 9
10@RunWith(SeleniumTestRunner.class)
11@SeleniumTest(driver = ChromeDriver.class)
12@Category(EndToEndTest.class)
13public class SubmitCreateTaskFormWithValidInformationTest {
14
15    private static final String DESCRIPTION = "This is a new lesson";
16    private static final String TITLE = "Write an extra lesson";
17
18    @SeleniumWebDriver
19    private WebDriver browser;
20
21    private CreateTaskPage createTaskPage;
22
23    @Before
24    public void configureTestCases() {
25        logUserIn();
26        createTaskPage = new CreateTaskPage(browser).open();
27    }
28
29    private void logUserIn() {
30        LoginPage loginPage = new LoginPage(browser).open();
31        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD);
32    }
33
34    @After
35    public void logUserOut() {
36        new NavigationBar(browser).logUserOut();
37    }
38}

Sixth, we have write a method that enters the title and description of the created task to the create task form and submits the form. This method helps us to write end-to-end tests which don’t have duplicate code.

After we have written this 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
 8import static com.testwithspring.intermediate.EndToEndTestUsers.*;
 9
10@RunWith(SeleniumTestRunner.class)
11@SeleniumTest(driver = ChromeDriver.class)
12@Category(EndToEndTest.class)
13public class SubmitCreateTaskFormWithValidInformationTest {
14
15    private static final String DESCRIPTION = "This is a new lesson";
16    private static final String TITLE = "Write an extra lesson";
17
18    @SeleniumWebDriver
19    private WebDriver browser;
20
21    private CreateTaskPage createTaskPage;
22
23    @Before
24    public void configureTestCases() {
25        logUserIn();
26        createTaskPage = new CreateTaskPage(browser).open();
27    }
28
29    private void logUserIn() {
30        LoginPage loginPage = new LoginPage(browser).open();
31        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD);
32    }
33
34    private TaskPage createTask() {
35        TaskForm createTaskForm = createTaskPage.getForm();
36        createTaskForm.typeTitle(TITLE);
37        createTaskForm.typeDescription(DESCRIPTION);
38        return createTaskForm.submitTaskForm();
39    }
40
41    @After
42    public void logUserOut() {
43        new NavigationBar(browser).logUserOut();
44    }
45}

Seventh, we have to write four end-to-end tests for the create task feature:

  1. We have to verify that it forwards the user to the view task page.
  2. We have to ensure that the view task page displays the description of the created task.
  3. We have to verify that the view task page displays the title of the created task.
  4. We have to ensure that the view task page displays the name of the person who created the task.

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 static com.testwithspring.intermediate.EndToEndTestUsers.*;
10import static org.assertj.core.api.Assertions.assertThat;
11
12@RunWith(SeleniumTestRunner.class)
13@SeleniumTest(driver = ChromeDriver.class)
14@Category(EndToEndTest.class)
15public class SubmitCreateTaskFormWithValidInformationTest {
16
17    private static final String DESCRIPTION = "This is a new lesson";
18    private static final String TITLE = "Write an extra lesson";
19
20    @SeleniumWebDriver
21    private WebDriver browser;
22
23    private CreateTaskPage createTaskPage;
24
25    @Before
26    public void configureTestCases() {
27        logUserIn();
28        createTaskPage = new CreateTaskPage(browser).open();
29    }
30
31    private void logUserIn() {
32        LoginPage loginPage = new LoginPage(browser).open();
33        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD);
34    }
35
36    @Test
37    public void shouldOpenViewTaskPage() {
38        TaskPage viewTaskPage = createTask();
39        assertThat(viewTaskPage.isOpenWithUnknownTaskId()).isTrue();
40    }
41
42    @Test
43    public void shouldShowDescriptionOfCreatedTask() {
44        TaskPage viewTaskPage = createTask();
45        assertThat(viewTaskPage.getTaskDescription()).isEqualTo(DESCRIPTION);
46    }
47
48    @Test
49    public void shouldShowTitleOfCreatedTask() {
50        TaskPage viewTaskPage = createTask();
51        assertThat(viewTaskPage.getTaskTitle()).isEqualTo(TITLE);
52    }
53
54    @Test
55    public void shouldShowCreatorNameOfCreatedTask() {
56        TaskPage viewTaskPage = createTask();
57        TaskLifeCycleFields lifeCycleFields = viewTaskPage
58                .getTaskLifeCycleFields();
59
60        assertThat(lifeCycleFields.getCreatorName()).isEqualTo(JohnDoe.NAME);
61    }
62
63    private TaskPage createTask() {
64        TaskForm createTaskForm = createTaskPage.getForm();
65        createTaskForm.typeTitle(TITLE);
66        createTaskForm.typeDescription(DESCRIPTION);
67        return createTaskForm.submitTaskForm();
68    }
69
70    @After
71    public void logUserOut() {
72        new NavigationBar(browser).logUserOut();
73    }
74}

One end-to-end test 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 NAME = "John Doe";
 9        public static final String PASSWORD = "user";
10    }
11}
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 create task feature. Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us two things:

  • It’s a good idea to create a page object that represents a form. 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 assertions for the id of the created task because we cannot know its expected value.

Get the source code from Github