With 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 aggregator and at several places, you'll find links to other articles and guides that explain the concepts in greater detail.
Unit and integration testing is an integral part of your everyday life as a developer. Especially for Spring Boot newcomers writing meaningful tests for their applications turns out to be an obstacle:
- Where to start my testing efforts?
- How can Spring Boot help me writing 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, e.g., your *Service
classes in isolation and mock every collaborator of your class under test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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 for this are your persistence layer or testing an HTTP client. 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, e.g., one controller in isolation. Other developers categorize them as integration tests as Spring support is involved. Howsoever 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 withMockMvc
@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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @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:
1 2 3 4 5 | // 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 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:
1 2 3 4 5 6 7 8 9 10 11 | <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:
1 2 3 4 5 6 7 8 9 10 11 | <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
:
1 2 | // 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @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:
- Write Spring Boot Integration Tests With Testcontainers
- Testing Spring Boot Applications with Kotlin and Testcontainers
- Initialization Strategies With Testcontainers For Integration Tests
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @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); } } |
You can find more information about Selenide as part of this blog post.
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:
1 2 3 4 5 6 | 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.
PS: For smart developers that expect a steep learning curve without investing much time on Stack Overflow and studying the documentation, I've created the Testing Spring Boot Applications Masterclass. You'll learn how to master different testing strategies and how to make the most of Spring Boot's excellent test support. Throughout this deep-dive course, you'll apply all concepts while testing a real-world application.
Happy unit and integration testing with Spring Boot,
Philip