In this blog you’ll get an overview of how unit and integration testing works with Spring Boot. On top of this, you’ll learn what Spring features and libraries to focus on first. This article acts as an overview and includes links to other resources, but you may want to investigate some concepts in greater detail.

Unit and integration testing are an integral part of your everyday life as a developer. But writing meaningful tests for your applications can be a tricky obstacle, especially for Spring Boot newcomers, so we’ll consider:

  • Where to start my testing efforts?
  • How can Spring Boot help me to write efficient tests?
  • What libraries should I use?

Unit Testing with Spring Boot

Unit tests build the foundation of your test strategy. Every Spring Boot project that you bootstrap with the Spring Initializr has a solid foundation for writing unit tests. There’s almost nothing to set up, as the Spring Boot Starter Test includes all necessary building blocks.

Apart from including and managing the version of Spring Test, this Spring Boot Starter includes and manages the version of the following libraries:

  • JUnit 4/5
  • Mockito
  • Assertion libraries like AssertJ, Hamcrest, JsonPath, etc.

You can find an introduction for this testing Swiss-army knife and the included testing libraries as part of this blog post.

Most of the time, your unit tests won’t need any specific Spring Boot or Spring Test feature as they’ll solely rely on JUnit and Mockito.

With your unit tests, you test (for example) your *Service classes in isolation and mock every collaborator of your class under test:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class) // register the Mockito extension
public class PricingServiceTest {

  @Mock // // Instruct Mockito to mock this object
  private ProductVerifier mockedProductVerifier;

  @Test
  public void shouldReturnCheapPriceWhenProductIsInStockOfCompetitor() {
    when(mockedProductVerifier.isCurrentlyInStockOfCompetitor("AirPods"))
      .thenReturn(true); //Specify what boolean value to return

    PricingService cut = new PricingService(mockedProductVerifier);

    assertEquals(new BigDecimal("99.99"), cut.calculatePrice("AirPods"));
  }
}

As you see from the import section of the test class above, there’s no include from Spring at all. Hence, you can apply techniques and knowledge from unit testing any other Java application.

That’s why it’s important to learn the fundamentals of both JUnit 4/5 and Mockito to make the most of your unit test.

For some parts of your application, unit testing won’t bring many benefits. Good examples of this are your persistence layer or testing an HTTP client. When testing such parts of your application you end up almost copying your implementation as you have to mock a lot of interaction with other classes.

A better approach here is to work with a sliced Spring Context that you can easily auto-configure with Spring Boot test annotations.

Tests With a Sliced Spring Context

On top of traditional unit tests, you can write tests with Spring Boot that target specific parts (slices) of your application. The Spring TestContext framework together with Spring Boot will tailor a Spring Context with just enough components for a particular test.

The purpose of these tests is to test a specific part of your application in isolation without starting the whole application. This improves both the test execution time and the need for an extensive test setup.

How to name such tests? In my opinion, they neither fall 100% in the unit or integration test category. Some developers refer to them as unit tests because they test (for example) one controller in isolation. Other developers categorize them as integration tests as Spring support is involved. However you name them, make sure to have a consistent understanding, at least in your team.

Spring Boot offers a lot of annotations to test various parts of your application in isolation: @JsonTest, @WebMvcTest, @DataMongoTest, @JdbcTest, etc.

All of them auto-configure a sliced Spring TestContext and include only Spring beans relevant to testing a particular part of your application. I’ve dedicated an entire article to introduce the most common of these annotations and explain their usage.

The two most important annotations (consider learning them first) are:

  • @WebMvcTest to effectively test your web-layer with MockMvc
  • @DataJpaTest to effectively test your persistence layer

There are also annotations available for more niche-parts of your application:

  • @JsonTest to verify JSON serialization and deserialization
  • @RestClientTest to test the RestTemplate
  • and @DataMongoTest to test MongoDB-related code

When using them, it’s important to understand which components are part of the TestContext and which aren’t. The Javadoc of each annotation explains the performed auto-configuration and purpose.

You can always enrich the auto-configure context for your test by either explicitly importing components with @Import or defining additional Spring Beans using @TestConfiguration:

@WebMvcTest(PublicController.class)
class PublicControllerTest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private MeterRegistry meterRegistry;

  @MockBean
  private UserService userService;

  @TestConfiguration
  static class TestConfig {

    @Bean
    public MeterRegistry meterRegistry() {
      return new SimpleMeterRegistry();
    }

  }
}

You can find further techniques to fix potential NoSuchBeanDefinitionException that you might face for such tests as part of this blog post.

JUnit 4 vs. JUnit 5 Pitfall

One big pitfall I encounter quite often when answering questions on Stack Overflow is the mix of JUnit 4 and JUnit 5 (JUnit Jupiter, to be more specific) within the same test. Using the API of different JUnit versions within the same test class will lead to unexpected outputs and failures.

It’s important to watch out for the import, especially for the @Test annotation:

// JUnit 4
import org.junit.Test;

// JUnit Jupiter (part of JUnit 5)
import org.junit.jupiter.api.Test;

Other indicators for JUnit 4 are: @RunWith, @Rule, @ClassRule, @Before, @BeforeClass, @After, @AfterClass.

With the help of JUnit 5’s vintage-engine your test suite can contain both JUnit 3/4 and JUnit Jupiter tests, but each test class can only use one particular JUnit version. Consider migrating your existing tests to make use of the various new features of JUnit Jupiter (parameterized tests, parallelization, extension model, etc.). You can gradually migrate your test suite as you can run JUnit 3/4 tests next to JUnit 5 tests.

The JUnit documentation includes JUnit 4 migration tips, and there are also tools available (JUnit Pioneer or this IntelliJ feature) to migrate tests automatically (e.g., imports or assertions).

Once you’ve migrated your test suite to JUnit 5, it’s important to exclude any occurrence of a vintage version of JUnit. Not everybody in your team might pay close attention to the test imports all the time. To avoid mixing different JUnit versions accidentally, excluding them from your project helps to always pick the correct imports:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
    <exclusion>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
  </exclusions>
</dependency>

Apart from the Spring Boot Starter Test, other test dependencies might also include older versions of JUnit:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>${testcontainers.version}</version>
  <exclusions>
    <exclusion>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </exclusion>
  </exclusions>
</dependency>

To avoid any (accidental) JUnit 4 dependency include in the future, you can use the Maven Enforcer Plugin and define it as a banned dependency. This will fail the build as soon as someone includes a new test dependency that pulls JUnit 4 transitively.

Please note that starting with Spring Boot 2.4.0, the Spring Boot Starter Test dependency no longer includes the vintage-engine by default.

Integration Tests With Spring Boot: @SpringBootTest

With integration tests, you usually test multiple components of your application in combination. Most of the time, you’ll use the @SpringBootTest annotation for this purpose and access your application from outside using either the WebTestClient or the TestRestTemplate.

@SpringBootTest will populate the entire application context for your test. When using this annotation, it’s important to understand the webEnvironment attribute. Without specifying this attribute, such tests won’t start the embedded servlet container (e.g., Tomcat) and use a mocked Servlet environment instead. Hence, your application won’t be accessible at a local port.

You can override this behavior by specifying either DEFINE_PORT or RANDOM_PORT:

// or DEFINED_PORT
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)

For integration tests that start the embedded Servlet container, you can then inject the port of your application and access it from outside using the TestRestTemplate or the WebTestClient:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ApplicationTests {

  @LocalServerPort
  private Integer port;

  @Autowired
  private TestRestTemplate testRestTemplate;

  @Test
  void accessApplication() {
    System.out.println(port);
  }
}

As the Spring TestContext framework will populate the entire application context you have to ensure that all dependent infrastructure components (e.g., database, messaging queues, etc.) are present.

This is where Testcontainers comes into play. Testcontainers will manage the lifecycle of any Docker container for your test:

@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationIT {

  @Container
  public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
    .withPassword("inmemory")
    .withUsername("inmemory");

  @DynamicPropertySource
  static void postgresqlProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
    registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
    registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
  }

  @Test
  public void contextLoads() {
  }

}

For a Testcontainers introduction, consider the following resources:

As soon as your application communicates with other systems you need a solution to mock this HTTP communication. This is quite often the case as you e.g. fetch data from a remote REST API or OAuth2 access tokens on application startup. With the help of WireMock, you can stub and prepare HTTP responses to simulate the existence of a remote system.

Furthermore, the Spring TestContext framework comes with a neat feature to cache and reuse and already started context. This can help to reduce build times and improve your feedback cycles drastically.

End-to-End Tests with Spring Boot

The purpose of end-to-end (E2E) tests is to validate the system from a user’s perspective. This includes tests for the main user journeys (e.g., place an order or create a new customer). Compared to integration tests, such tests usually involve the user interface (if there is one).

You can also perform E2E tests against a deployed version of the application on, e.g., a dev or staging environment before proceeding with the production deployment.

For applications that use server-side rendering (e.g., Thymeleaf) or a self-contained systems approach, where the Spring Boot backend serves the frontend, you can use @SpringBootTest for these tests.

As soon as you need to interact with a browser, Selenium is usually the default choice. If you’ve worked with Selenium for quite some time, you might find yourself implementing the same helper functions over and over. For a better developer experience and fewer headaches when writing tests that involve browser interaction, consider Selenide. Selenide is an abstraction on top of Selenium’s low-level API to write stable and concise browser tests.

The following test showcases how to access and test a public page of a Spring Boot application using Selenide:

@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class BookStoreTestcontainersWT {

  @LocalServerPort
  private Integer port;

  @Test
  public void shouldDisplayBook() {

    Configuration.timeout = 2000;
    Configuration.baseUrl = "http://localhost:" + port;

    open("/book-store");

    $(By.id("all-books")).shouldNot(Condition.exist);
    $(By.id("fetch-books")).click();
    $(By.id("all-books")).shouldBe(Condition.visible);
  }
}

For the infrastructure components that you need to start for your E2E tests, Testcontainers plays a big role again. In case you have to start multiple Docker containers, the Docker Compose Module of Testcontainers comes in handy:

public static DockerComposeContainer<?> environment =
  new DockerComposeContainer<>(new File("docker-compose.yml"))
    .withExposedService("database_1", 5432, Wait.forListeningPort())
    .withExposedService("keycloak_1", 8080, Wait.forHttp("/auth").forStatusCode(200)
      .withStartupTimeout(Duration.ofSeconds(30)))
    .withExposedService("sqs_1", 9324, Wait.forListeningPort());

Summary

Spring Boot offers excellent support for both unit and integration testing. It makes testing a first-class citizen as every Spring Boot project includes the Spring Boot Starter Test. This starter prepares your basic testing toolbox with essential testing libraries.

On top of this, the Spring Boot test annotations make writing tests for different parts of your application a breeze. You’ll get a tailor-made Spring TestContext with only relevant Spring beans.

To get familiar with unit and integration tests for your Spring Boot projects, consider the following steps:

  • Learn and understand the basics of JUnit and Mockito
  • Avoid the JUnit 4 vs. JUnit 5 pitfall.
  • Make yourself familiar with the different Spring Boot test annotations that auto-configure a sliced context.
  • WireMock, Testcontainers, and Selenide will support your integration and end-to-end testing efforts.
  • Understand how the Spring TestContext Caching can help to reduce the overall execution time of your test suite.

In case your test is still not doing what you expect, don’t desperately cut your testing efforts with the excuse that Spring Boot is too much magic. There’s great material available on both the Spring Documentation and on various blogs.

Furthermore, the community activity on Stack Overflow for tags like spring-test, spring-boot-test, or spring-test-mvc is quite good, and there’s a high chance you get help. I’m also frequently answering testing-related questions on Stack Overflow.

Happy unit and integration testing with Spring Boot, Philip

This is the second in a series of articles by Philip Riecks, an independent software consultant from Germany who specializes in educating Java developers about Spring Boot, AWS and testing. A version of this article was originally posted by Philip on rieckpil.de