Writing End-to-End Tests With Spock Framework – Forms

After we have finished this lesson, we:

  • Can write end-to-end tests for features that submit a form.
  • Understand how we should structure our specification classes when we specify the behavior of the system under specification.
This lesson assumes that:

Important note:

You must finish the lesson: Writing End-to-End Tests for Spring Web Applications – Submitting a Form before you continue this lesson. That lesson describes the requirements of the tested feature, explains how you can create the required page objects, and describes how you can write the required constant classes.

Because I don’t want to repeat myself, I won’t explain these things in this lesson. The goal of this lesson is to simply demonstrate how you can write end-to-end tests for features that submit a form.

Specifying the Behavior of the Create Task Feature

We can specify the expected behavior of the create task feature by following these steps:

First, we have to create a new specification class and configure Selenium WebDriver. After we have done this, the source code of our specification class looks as follows:

import org.junit.experimental.categories.Category
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import spock.lang.Shared
import spock.lang.Specification

@SeleniumTest(driver = ChromeDriver.class)
@Category(EndToEndTest.class)
class CreateTaskSpec extends Specification {

    @SeleniumWebDriver
    @Shared WebDriver browser
}

Second, we have to add two constants to our specification class. These constants contain the title and description of the created task.

After we have added these constants to our specification class, its source code looks as follows:

import org.junit.experimental.categories.Category
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import spock.lang.Shared
import spock.lang.Specification

@SeleniumTest(driver = ChromeDriver.class)
@Category(EndToEndTest.class)
class CreateTaskSpec extends Specification {

    private static final DESCRIPTION = 'This is a new lesson'
    private static final TITLE = 'Write an extra lesson'

    @SeleniumWebDriver
    @Shared WebDriver browser
}
By the way, we added these constants directly to our specication class because they aren’t used by other specification classes.

Third, we have to add a CreateTaskPage field to our specification class. Our feature method uses this field when it opens the create task page and submits the create task form.

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

import org.junit.experimental.categories.Category
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import spock.lang.Shared
import spock.lang.Specification

@SeleniumTest(driver = ChromeDriver.class)
@Category(EndToEndTest.class)
class CreateTaskSpec extends Specification {

    private static final DESCRIPTION = 'This is a new lesson'
    private static final TITLE = 'Write an extra lesson'

    @SeleniumWebDriver
    @Shared WebDriver browser

    CreateTaskPage createTaskPage
}

Fourth, we have to add a setup() method to our specification class and implement this method by creating a new CreateTaskPage object. After we have created this object, we have to store the created object in the createTaskPage field.

After we have implemented the setup() method, the source code of our specification class looks as follows:

import org.junit.experimental.categories.Category
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import spock.lang.Shared
import spock.lang.Specification

@SeleniumTest(driver = ChromeDriver.class)
@Category(EndToEndTest.class)
class CreateTaskSpec extends Specification {

    private static final DESCRIPTION = 'This is a new lesson'
    private static final TITLE = 'Write an extra lesson'

    @SeleniumWebDriver
    @Shared WebDriver browser

    CreateTaskPage createTaskPage

    def setup() {
        createTaskPage = new CreateTaskPage(browser)
    }
}

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

After we have implemented the cleanup() method, the source code of our specification class looks as follows:

import org.junit.experimental.categories.Category
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import spock.lang.Shared
import spock.lang.Specification

@SeleniumTest(driver = ChromeDriver.class)
@Category(EndToEndTest.class)
class CreateTaskSpec extends Specification {

    private static final DESCRIPTION = 'This is a new lesson'
    private static final TITLE = 'Write an extra lesson'

    @SeleniumWebDriver
    @Shared WebDriver browser

    CreateTaskPage createTaskPage

    def setup() {
        createTaskPage = new CreateTaskPage(browser)
    }

    def cleanup() {
        new NavigationBar(browser).logUserOut()
    }
}

Sixth, we have to write a feature method that specifies the behavior of the create task feature when a registered user creates a new task by using valid information. We can write this feature method by following these steps:

  1. Add a new feature method to our specification class.
  2. Define a variable that contains the page object which represents the view task page that is opened after a new task has been created. We must use this variable when we write assertions for the expected results.
  3. Open the login page and log the user in.
  4. Open the create task page. After we have opened this page, we have to enter the title and description of the created task to the create task form and submit the form. Also, we must store the returned page object in the shownPage variable.
  5. Ensure that the system under specification opened the view task page.
  6. Verify that the view task page displays the title of the created task.
  7. Ensure that the view task page displays the description of the created task.
  8. Verify that the view task page doesn’t display the name of the assignee.
  9. Ensure that the view task page displays the name of the person who created the task.
  10. Verify that the view task page displays the name of the person who modified the task.
  11. Ensure that the view task page doesn’t display the lifecycle fields of a closed task.

After we have written this feature method, the source code of our specification class looks as follows:

import org.junit.experimental.categories.Category
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import spock.lang.Shared
import spock.lang.Specification

import static com.testwithspring.master.EndToEndTestUsers.JohnDoe

@SeleniumTest(driver = ChromeDriver.class)
@Category(EndToEndTest.class)
class CreateTaskSpec extends Specification {

    private static final DESCRIPTION = 'This is a new lesson'
    private static final TITLE = 'Write an extra lesson'

    @SeleniumWebDriver
    @Shared WebDriver browser

    CreateTaskPage createTaskPage

    def setup() {
        createTaskPage = new CreateTaskPage(browser)
    }

    def 'Create a new task by using valid information as a registered user'() {

        TaskPage shownPage

        given: 'A user is authenticated successfully'
        LoginPage loginPage = new LoginPage(browser).open()
        loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD)

        when: 'A user opens the create task page and submits the create task form by using valid information'
        def createTaskForm = createTaskPage.open().getForm()
        createTaskForm.typeTitle(TITLE)
        createTaskForm.typeDescription(DESCRIPTION)
        shownPage = createTaskForm.submitTaskForm()

        then: 'Should open the view task page'
        shownPage.isOpenWithUnknownTaskId()

        and: 'Should display the title of the created task'
        shownPage.getTaskTitle() == TITLE

        and: 'Should display the description of the created task'
        shownPage.getTaskDescription() == DESCRIPTION

        and: 'Should not display the assignee name'
        def lifecycleFields = shownPage.getTaskLifecycleFields()
        !lifecycleFields.isAssigneeNameVisible()

        and: 'Should display the name of the creator'
        lifecycleFields.getCreatorName() == JohnDoe.NAME

        and: 'Should display the name of the modifier'
        lifecycleFields.getModifierName() == JohnDoe.NAME

        and: 'Should not display the lifecycle fields of a closed task'
        !lifecycleFields.areClosedTaskFieldsVisible()
    }

    def cleanup() {
        new NavigationBar(browser).logUserOut()
    }
}

We can now write end-to-end tests for features that submit a form. Next, we will find out how we should structure our specification classes.

Structuring Our Specification Classes

It’s technically possible to write a feature method which specifies the behavior of the system under specification when validation fails and when validation is successful. That being said, we have to answer to the following questions before we decide to use this approach:

  • Does our feature method become too long? Because we have to add multiple when and then blocks to our feature method, our feature method can become quite long and this makes it hard to read.
  • Is it too cumbersome to provide a clean initial state for each test case? If we are writing end-to-end tests for a complex feature, providing a clean initial state for each test case will most likely take some work. In other words, we have to decide if this extra work makes our feature methods both hard to read and write.

When we think about these two questions, it’s clear that writing only one feature method is practical only when we are writing end-to-end tests for a simple feature. In other words, most of the time we should structure our specification classes by following these rules:

  • We should create one specification class which contains the feature methods that specify the behavior of the system under specification when validation fails.
  • We should create one specification class which contains the feature methods that specify the behavior of the system under specification when validation is successful.
  • We should write one feature method per user role. This helps us to minimize the number of specification classes, and write end-to-end tests which have a clean and logical structure. Also, if we follow this rule, the created HTML documentation looks logical as well.
We don’t necessarily want to create a specification class that specifies the behavior of the system under specification when validation fails. This decision depends on our testing strategy. For example, if we have already written unit and integration tests which ensure that the system under specification is working as expected when validation fails, I think that writing end-to-end tests for this scenario is not worth it.

We can now write end-to-end tests for features that submit a form and structure our specification classes in the “correct” way. Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us three things:

  • We should create one specification class that specifies the behavior of the system under specification when validation fails.
  • We should create one specification class that specifies the behavior of the system under specification when validation is successful.
  • We should write one feature method per user role.

Get the source code from Github