Spring Boot Integration Testing with Selenium
Web integration tests allow integration testing of Spring Boot application without any mocking. By using @WebIntegrationTest
and @SpringApplicationConfiguration
we can create tests that loads the application and listen on normal ports. This small addition to Spring Boot makes much easier to create integration tests with Selenium WebDriver.
Test Dependencies
The application that we will be testing is a simple Spring Boot / Thymeleaf application with spring-boot-starter-web
, spring-boot-starter-thymeleaf
and spring-boot-starter-actuator
dependencies. See references for the link to the GitHub project.
The test dependencies are:
Update (21/3/2016): Dependencies in the project got updated (io.spring.platform, bootstrap, jquery, assertj and selenium). Link to source code in references
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>2.45.0</version>
<scope>test</scope>
</dependency>
Web Integration Test
With classic Spring Test, using MockMvc
, you would create test like below:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class HomeControllerClassicTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void verifiesHomePageLoads() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}
@SpringApplicationConfiguration
extends capabilities of @ContextConfiguration
and loads application context for integration test. To create a test without mocked environment we should define our test using @WebIntegrationTest
annotation:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest(value = "server.port=9000")
public class HomeControllerTest {
}
This will start full application within JUnit test, listening on port 9000
. Having such test we can easily add Selenium and execute real functional tests using a browser (will not work in headless environment, unless we use HtmlUnit driver - but this is beyond scope of this article).
Adding Selenium
Adding Selenium to the test is very simple, but I wanted to achieve a bit more than that hence I created a custom annotation to mark my tests as Selenium tests. I also configured it the way it allows injecting WebDriver
to the test instance:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest(value = "server.port=9000")
@SeleniumTest(driver = ChromeDriver.class, baseUrl = "http://localhost:9000")
public class HomeControllerTest {
@Autowired
private WebDriver driver;
}
@SeleniumTest
@SeleniumTest
is a custom annotation:
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@TestExecutionListeners(
listeners = SeleniumTestExecutionListener.class,
mergeMode = MERGE_WITH_DEFAULTS)
public @interface SeleniumTest {
Class<? extends WebDriver> driver() default FirefoxDriver.class;
String baseUrl() default "http://localhost:8080";
}
The annotation uses adds test execution listener that will create a WebDriver
instance that can be used in the integration test. TestExecutionListener
defines a listener API for reacting to test execution events. It can be used to instrument the tests. Example implementations in Spring Test are used to support test-managed transactions or dependency injection into test instances, for instance.
TestExecutionListener
Note: Some parts of the code of SeleniumTestExecutionListener
are skipped for better readability.
SeleniumTestExecutionListener
provides way to inject configured WebDriver
into test instances. The driver instance will be created only once and the driver used can be simply changed with @SeleniumTest
annotation. The most important thing was to register the driver with Bean Factory.
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
ApplicationContext context = testContext.getApplicationContext();
if (context instanceof ConfigurableApplicationContext) {
SeleniumTest annotation = findAnnotation(
testContext.getTestClass(), SeleniumTest.class);
webDriver = BeanUtils.instantiate(annotation.driver());
// register the bean with bean factory
}
}
Before each test method base URL of the application will be opened by a WebDriver
:
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
SeleniumTest annotation = findAnnotation(
testContext.getTestClass(), SeleniumTest.class);
webDriver.get(annotation.baseUrl());
}
In addition, on every failure a screenshot will be generated:
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
if (testContext.getTestException() == null) {
return;
}
File screenshot = ((TakesScreenshot) webDriver).getScreenshotAs(OutputType.FILE);
// do stuff with the screenshot
}
After each test the driver will be closed:
@Override
public void afterTestClass(TestContext testContext) throws Exception {
if (webDriver != null) {
webDriver.quit();
}
}
This is just an example. Very simple implementation. We could extend the capabilities of the annotation and the listener.
The test
Running the below test will start the Chrome browser and execute some simple checks with Selenium:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest(value = "server.port=9000")
@SeleniumTest(driver = ChromeDriver.class, baseUrl = "http://localhost:9000")
public class HomeControllerTest {
@Autowired
private WebDriver driver;
private HomePage homePage;
@Before
public void setUp() throws Exception {
homePage = PageFactory.initElements(driver, HomePage.class);
}
@Test
public void containsActuatorLinks() {
homePage.assertThat()
.hasActuatorLink("autoconfig", "beans", "configprops", "dump", "env", "health", "info", "metrics", "mappings", "trace")
.hasNoActuatorLink("shutdown");
}
@Test
public void failingTest() {
homePage.assertThat()
.hasNoActuatorLink("autoconfig");
}
}
The test uses simple page object with custom AssertJ assertions. You can find the full source code in GitHub. See references.
In case of a failure, the screenshot taken by the driver, will be stored in appropriate directory.
Summary
Integration testing of fully loaded Spring Boot application is possible in regular JUnit test thanks to @WebIntegrationTest
and @SpringApplicationConfiguration
annotations. Having the application running within a test opens a possibility to hire Selenium and run functional tests using the browser. If you combine it with profiles and some more features of Spring Test (e.g. @Sql
, @SqlConfig
) you may end up with quite powerful yet simple solution for your integration tests.