Configuring Our End-to-End Tests - Spring Boot

After we have finished this lesson, we

  • Understand how we can start our Spring Boot application before our end-to-end tests are run.
  • Can create a custom test execution listener that configures Selenium WebDriver, injects it into our test class, and cleans up the reserved resources.
  • Know how we can configure our end-to-end tests by using our custom test execution listener.

Creating and Configuring Selenium WebDriver

The web applications that use Spring Framework require a servlet container. This means that when we write end-to-end tests for a web application that uses Spring Framework, we have to deploy our application to a servlet container and start the servlet container before we can run our end-to-end tests.

However, because a Spring Boot web application doesn’t require a servlet container, we can start our Spring Boot application by leveraging the testing support provided by Spring Boot. This means that we write our end-to-end tests by using the features provided by the Spring Test framework and Spring Boot.

That being said, we still have to figure out a way to:

  1. Create and configure a new WebDriver object before our end-to-end tests are run.
  2. Quit the WebDriver instance and close all browser windows after our end-to-end tests have been run.

Also, we don’t want to use the “normal” setup and teardown methods provided by JUnit because using these methods makes our end-to-end tests hard to read, write, and maintain.

Instead, we will configure our end-to-end tests by creating a custom setup annotation and a custom TestExecutionListener. Let’s get started.

Credits:

I got this idea from a blog post titled: Spring Boot Integration Testing with Selenium by Rafał Borowiec.

Creating the Setup Annotation

The first thing that we have to do is to create the custom setup annotation that we use when we configure our end-to-end tests. We can create this annotation by following these steps:

  1. Create a @SeleniumTest annotation and ensure that we can add this annotation to a test class.
  2. Register a new test execution listener called SeleniumTestExecutionListener and ensure that this test execution listener is merged with the default test execution listeners provided by the Spring Test framework. This test execution listener creates a new WebDriver object before our end-to-end tests are run and quits the created WebDriver instance after our end-to-end tests have been run.
  3. Add a driver attribute to our custom annotation. This attribute configures the type of the created WebDriver object, and the default value of this attribute is ChromeDriver.class.

After we have created the @SeleniumTest annotation, its source code looks as follows:

 1import org.openqa.selenium.WebDriver;
 2import org.openqa.selenium.chrome.ChromeDriver;
 3import org.springframework.test.context.TestExecutionListeners;
 4
 5import java.lang.annotation.*;
 6
 7@Documented
 8@Inherited
 9@Retention(RetentionPolicy.RUNTIME)
10@Target(ElementType.TYPE)
11@TestExecutionListeners(listeners = {SeleniumTestExecutionListener.class},
12        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
13)
14public @interface SeleniumTest {
15
16    Class<? extends WebDriver> driver() default ChromeDriver.class;
17}
I admit that this annotation is not nearly as useful as it could be. I decided to keep it as simple as possible because I have no idea what kind of configuration options you want to use. That being said, you can add the missing configuration options to this annotation and configure the created WebDriver object by following the instructions given in this lesson.

Next, we have to create our custom test execution listener. Let’s find out how we can do it.

Creating a Custom Test Execution Listener

We have to create a test execution listener that fulfills these three requirements:

  • The name of our test execution listener must be SeleniumTestExecutionListener.
  • It must create a new WebDriver bean before the @Before method is run.
  • It must quit the created WebDriver instance after the @After method has been run.

In other words, we want to create a test execution listener that allows us to inject a WebDriver bean into our test class and use this bean in our setup, test, and teardown methods.

We can create our custom test execution listener by following these steps:

First, we have to create a class called SeleniumTestExecutionListener that extends the AbstractTestExecutionListener class. After we have created this class, its source code looks as follows:

1import org.springframework.test.context.support.AbstractTestExecutionListener;
2
3public class SeleniumTestExecutionListener extends AbstractTestExecutionListener {
4
5}

Second, we have to add a WebDriver field to the SeleniumTestExecutionListener class. This field contains the WebDriver object that is used by our end-to-end tests.

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

1import org.openqa.selenium.WebDriver;
2import org.springframework.test.context.support.AbstractTestExecutionListener;
3
4public class SeleniumTestExecutionListener extends AbstractTestExecutionListener {
5
6    private WebDriver webDriver;
7
8}

Third, we have to implement the getOrder() method of the Ordered interface. Our implementation must return Ordered.HIGHEST_PRECEDENCE which ensures that our test execution listener is ordered (i.e. invoked) before the default listeners provided by the Spring Test framework.

After we have implemented this method, the source code of the SeleniumTestExecutionListener class looks as follows:

 1import org.openqa.selenium.WebDriver;
 2import org.springframework.core.Ordered;
 3import org.springframework.test.context.support.AbstractTestExecutionListener;
 4
 5public class SeleniumTestExecutionListener extends AbstractTestExecutionListener {
 6
 7    private WebDriver webDriver;
 8
 9    @Override
10    public int getOrder() {
11        return Ordered.HIGHEST_PRECEDENCE;
12    }
13}

Fourth, we have to implement the prepareTestInstance() method of the TestExecutionListener interface. We can implement this method by following these steps:

  1. If the WebDriver field has already been initialized, we must ensure that this method returns without doing anything.
  2. Get the ApplicationContext that is associated with our test context and ensure that it is an instance of the ConfigurableApplicationContext. If this is not the case, we cannot register the created WebDriver to the bean factory, and we must ensure that this method returns without doing anything else.
  3. Create a new WebDriver object by using the configuration that is provided by the @SeleniumTest annotation and set a reference to the created object to the WebDriver field.
  4. Register the created WebDriver object to the bean factory. This allows us to inject WebDriver objects into our end-to-end test classes.

After we have implemented the prepareTestInstance() method, the source code of our test execution listener class looks as follows:

 1import org.openqa.selenium.WebDriver;
 2import org.springframework.beans.BeanUtils;
 3import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 4import org.springframework.context.ApplicationContext;
 5import org.springframework.context.ConfigurableApplicationContext;
 6import org.springframework.core.Ordered;
 7import org.springframework.core.annotation.AnnotationUtils;
 8import org.springframework.test.context.TestContext;
 9import org.springframework.test.context.support.AbstractTestExecutionListener;
10
11public class SeleniumTestExecutionListener extends AbstractTestExecutionListener {
12
13    private WebDriver webDriver;
14
15    @Override
16    public int getOrder() {
17        return Ordered.HIGHEST_PRECEDENCE;
18    }
19
20    @Override
21    public void prepareTestInstance(TestContext testContext) throws Exception {
22        if (webDriver != null) {
23            return;
24        }
25        ApplicationContext context = testContext.getApplicationContext();
26        if (context instanceof ConfigurableApplicationContext) {
27            webDriver = createConfiguredWebDriver(testContext);
28            registerWebDriverBean(context, webDriver);
29        }
30    }
31
32    private WebDriver createConfiguredWebDriver(TestContext testContext) {
33        SeleniumTest seleniumConfig = AnnotationUtils.findAnnotation(
34                testContext.getTestClass(), SeleniumTest.class);
35        return BeanUtils.instantiate(seleniumConfig.driver());
36    }
37
38    private void registerWebDriverBean(ApplicationContext targetContext, WebDriver dependency) {
39        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) targetContext;
40        ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory();
41        bf.registerResolvableDependency(WebDriver.class, dependency);
42    }
43}

Fifth, we have to implement the afterTestClass() method of the TestExecutionListener interface. Our implementation simply quits the created WebDriver instance.

After we have implemented this method, the source code of the SeleniumTestExecutionListener class looks as follows:

 1import org.openqa.selenium.WebDriver;
 2import org.springframework.beans.BeanUtils;
 3import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 4import org.springframework.context.ApplicationContext;
 5import org.springframework.context.ConfigurableApplicationContext;
 6import org.springframework.core.Ordered;
 7import org.springframework.core.annotation.AnnotationUtils;
 8import org.springframework.test.context.TestContext;
 9import org.springframework.test.context.support.AbstractTestExecutionListener;
10
11public class SeleniumTestExecutionListener extends AbstractTestExecutionListener {
12
13    private WebDriver webDriver;
14
15    @Override
16    public int getOrder() {
17        return Ordered.HIGHEST_PRECEDENCE;
18    }
19
20    @Override
21    public void prepareTestInstance(TestContext testContext) throws Exception {
22        if (webDriver != null) {
23            return;
24        }
25        ApplicationContext context = testContext.getApplicationContext();
26        if (context instanceof ConfigurableApplicationContext) {
27            webDriver = createConfiguredWebDriver(testContext);
28            registerWebDriverBean(context, webDriver);
29        }
30    }
31
32    private WebDriver createConfiguredWebDriver(TestContext testContext) {
33        SeleniumTest seleniumConfig = AnnotationUtils.findAnnotation(
34                testContext.getTestClass(), SeleniumTest.class);
35        return BeanUtils.instantiate(seleniumConfig.driver());
36    }
37
38    private void registerWebDriverBean(ApplicationContext targetContext, WebDriver dependency) {
39        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) targetContext;
40        ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory();
41        bf.registerResolvableDependency(WebDriver.class, dependency);
42    }
43
44    @Override
45    public void afterTestClass(TestContext testContext) throws Exception {
46        if (webDriver != null) {
47            webDriver.quit();
48        }
49    }
50}

We have now implemented our custom test execution listener. Let’s find out how we can use it when we configure our end-to-end tests.

Configuring Our End-to-End Tests

Let’s create a simple test class that contains one end-to-end test. We can write this test class by following these steps:

  1. Create a new test class and ensure that all test methods of this test class belong to the EndToEndTest category.
  2. Ensure that our test methods are run by the SpringJUnit4ClassRunner.
  3. Load the application context of our Spring Boot application.
  4. Ensure that our Spring Boot application is started when its application context is loaded. Also, remember to specify the port that is listened by the started Spring Boot application. Because our example uses the default port (8080), we don’t have to make any other changes to our configuration.
  5. Annotate the test class with the @SeleniumTest annotation and ensure that the type of the created WebDriver object is ChromeDriver.
  6. Specify that the Spring profile called: endToEndTest is used when the test methods of our test class are run.
  7. Add a WebDriver field to our test class and ensure that the real WebDriver object is provided by the Spring container.
  8. Write a simple test method that opens the front page of our web application and ensures that it has the correct title.

After we have written our test class, its source code looks as follows:

 1import org.junit.Test;
 2import org.junit.experimental.categories.Category;
 3import org.junit.runner.RunWith;
 4import org.openqa.selenium.WebDriver;
 5import org.openqa.selenium.chrome.ChromeDriver;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.boot.test.context.SpringBootTest;
 8import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
 9import org.springframework.test.context.ActiveProfiles;
10import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
11
12import static org.assertj.core.api.Assertions.assertThat;
13
14@RunWith(SpringJUnit4ClassRunner.class)
15@SpringBootTest(classes = MessageApplication.class,
16		webEnvironment = WebEnvironment.DEFINED_PORT
17)
18@SeleniumTest(driver = ChromeDriver.class)
19@ActiveProfiles(Profiles.END_TO_END_TEST)
20@Category(EndToEndTest.class)
21public class ShowMessagePageTest {
22
23    @Autowired
24    private WebDriver browser;
25
26    @Test
27    public void shouldOpenFrontPage() {
28        browser.get("http://localhost:8080");
29        assertThat(browser.getTitle()).isEqualTo("Hello World!");
30    }
31}

When we run our end-to-end test, we notice that it passes:

1$ mvn clean verify -P end-to-end-test
2
3Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.73 sec -
4in com.testwithspring.intermediate.message.ShowMessagePageTest
5
6Results :
7
8Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us three things:

  • We can start our Spring Boot application by changing the value of the @SpringBootTest annotation’s webEnvironment attribute.
  • We can create a custom test execution listener by extending the AbstractTestExecutionListener class.
  • If we want to merge our custom test execution listener with the default test execution listeners provided by the Spring Test framework, we have configure our test execution listener by using the @TestExecutionListeners annotation. When we use this annotation, we have to change the value of its mergeMode attribute to TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS.

Get the source code from Github