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.
This lesson assumes that:
- You can configure your integration tests
- You can run end-to-end tests with Maven or Gradle
- You are familiar with Selenium
- You can install the required prerequisites
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:
- Create and configure a new
WebDriverobject before our end-to-end tests are run. - Quit the
WebDriverinstance 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:
- Create a
@SeleniumTestannotation and ensure that we can add this annotation to a test class. - Register a new test execution listener called
SeleniumTestExecutionListenerand 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 newWebDriverobject before our end-to-end tests are run and quits the createdWebDriverinstance after our end-to-end tests have been run. - Add a
driverattribute to our custom annotation. This attribute configures the type of the createdWebDriverobject, and the default value of this attribute isChromeDriver.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}
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
WebDriverbean before the@Beforemethod is run. - It must quit the created
WebDriverinstance after the@Aftermethod 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:
- If the
WebDriverfield has already been initialized, we must ensure that this method returns without doing anything. - Get the
ApplicationContextthat is associated with our test context and ensure that it is an instance of theConfigurableApplicationContext. If this is not the case, we cannot register the createdWebDriverto the bean factory, and we must ensure that this method returns without doing anything else. - Create a new
WebDriverobject by using the configuration that is provided by the@SeleniumTestannotation and set a reference to the created object to theWebDriverfield. - Register the created
WebDriverobject to the bean factory. This allows us to injectWebDriverobjects 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}
Additional Reading:
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:
- Create a new test class and ensure that all test methods of this test class belong to the
EndToEndTestcategory. - Ensure that our test methods are run by the
SpringJUnit4ClassRunner. - Load the application context of our Spring Boot application.
- 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.
- Annotate the test class with the
@SeleniumTestannotation and ensure that the type of the createdWebDriverobject isChromeDriver. - Specify that the Spring profile called: endToEndTest is used when the test methods of our test class are run.
- Add a
WebDriverfield to our test class and ensure that the realWebDriverobject is provided by the Spring container. - 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}
Additional Reading:
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
@SpringBootTestannotation’swebEnvironmentattribute. - We can create a custom test execution listener by extending the
AbstractTestExecutionListenerclass. - 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
@TestExecutionListenersannotation. When we use this annotation, we have to change the value of itsmergeModeattribute toTestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS.