With this guide, we'll investigate the powerful @SpringBootTest annotation for writing Spring Boot integration tests. If you're new to testing the Spring Boot applications, start with the testing overview and get a basic understanding of the testing swiss-army knife Spring Boot Starter Test.
While the Spring ecosystem seamlessly integrates various technologies and tools, it also provides excellent testing capabilities. However, when starting to work with Spring Boot, one might get easily overwhelmed when testing their application. The different sliced context setups might not be trivial in the beginning. The chances are high that not-so-optimal testing approaches are taken because a birds-eye view of the different testing support mechanisms is missing.
The @SpringBootTest Annotation Under the Hood
Let's start with the foundation and understand what the @SpringBootTest
annotation is all about.
When starting to work with Spring Boot, it's usually the first test annotation to stumble over. That's because many testing tutorials use it, and due to the name, it implies that one can use it to test a Spring Boot application.
Every new Spring Boot project comes with an initial test inside src/test/resources
that uses this annotation:
1 2 3 4 5 6 7 8 9 10 11 | import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class DemoApplicationTest { @Test void contextLoads() { } } |
As the test method name applies, this initial test ensures the Spring ApplicationContext
can successfully start without any dependency injection errors (aka. NoSuchBeanDefinitionException) or missing configuration properties.
This default test is already one of our most important integration tests. We can detect potential Spring Boot startup issues early on by launching the entire context during test execution.
The internals of @SpringBootTest
reveal how this annotation works under the hood:
1 2 3 4 5 6 7 8 9 10 11 | @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @BootstrapWith(SpringBootTestContextBootstrapper.class) @ExtendWith({SpringExtension.class}) public @interface SpringBootTest { // further attributes } |
Starting from the bottom, we can see that the @SpringBootTest
meta-annotation registers the JUnit Jupiter (part of JUnit 5) SpringExtension
. This extension is essential for the seamless integration of our test framework with Spring. Among other things, we'll be able to inject (@Autowired
) beans from the TestContext to our test classes.
The next annotation (@BootstrapWith
) does the entire heavy lifting and starts the context for our test. In short, it searches for our main Spring Boot entry class (annotated with @SpringBootApplication
) to retrieve our context configuration and start it accordingly:
1 2 3 4 5 6 7 8 | @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } |
This will then trigger the component scanning mechanism and apply all auto-configurations.
The last four annotations (@Inherited
, @Documented
, @Retention
, @Target
) are Java-specific annotations from java.lang.annotation
. They're used to define metadata about the custom annotation to limit the scope where one can apply the annotation (e.g., class or method level) and its retention period.
As part of using the @SpringBootTest
annotation for our integration test, we can further tweak the environment and define which type of WebEnvironment
to create for a particular test.
Configuration Options for Integration Tests With @SpringBootTest
Let's take a look at the different configuration values for the webEnvironment
attribute of the @SpringBootTest
annotation.
This value impacts the type of context we test against. Furthermore, it defines if Spring Test starts the embedded servlet container (Tomcat, Jetty, Undertow – Tomcat is the default).
The Default: WebEnvironment.MOCK
MOCK
is the default configuration that is applied in case we don't specify any value. Spring Test will create a WebApplicationContext
with a mocked servlet environment (when using Spring MVC) for our test.
That's similar to what we get when using MockMvc
and writing tests for our Spring Web MVC controllers using @WebMvcTest
.
The difference between @WebMvcTest
and this setup is the number of Spring Beans that are part of the Spring TestContext. While @WebMvcTest
only populates web-related beans, with @SpringBootTest
we populate the entire context.
1 2 3 | @SpringBootTest(webEnvironment = WebEnvironment.MOCK) class ApplicationIT { } |
However, this configuration won't start the embedded servlet container.
Most Commonly Used for @SpringBootTest: WebEnvironment.RANDOM_PORT
With this configuration, Spring creates a WebApplicationContext
for our integration test and starts the embedded servlet container on a random ephemeral port.
This also brings up the management server on a random port in case we expose our Actuator endpoints from a different port. If we don't override management.server.port
in any of our configuration files, Spring will choose the same port for the management part as for our application.
Furthermore, we can inject auto-configured HTTP (WebTestClient
or RestTestTemplate
) clients that point to the started application. There's no need to fiddle around with the port and hostname when accessing our started application using these clients:
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 27 28 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ApplicationIT { @Autowired private WebTestClient webTestClient; // available with Spring WebFlux @Autowired private TestRestTemplate testRestTemplate; // available with Spring Web MVC @LocalServerPort private Integer port; @Test void httpClientExample() { // no need for this WebClient webClient = WebClient.builder() .baseUrl("http://localhost:" + port) .build(); this.webTestClient .get() .uri("/api/customers") .exchange() .expectStatus().is2xxSuccessful(); } } |
In case we do need information about the chosen port for our application and the management server, we can inject both port numbers into our test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ApplicationIT { @LocalServerPort private Integer port; @LocalManagementPort private Integer managementPort; @Test void printPortsInUse() { System.out.println(port); System.out.println(managementPort); } } |
This configuration value should be our default choice when writing integration tests that verify our application from the outside by accessing one of our HTTP endpoints.
Rarely Applicable for Integration Tests: WebEnvironment.DEFINED_PORT
With DEFINED_PORT
we instruct Spring to start the embedded servlet container on the pre-defined port. By default, that's port 8080, but we can configure this port with the server.port
property.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @SpringBootTest( webEnvironment = WebEnvironment.DEFINED_PORT, properties = { "server.port=8042", "management.server.port=9042" }) class ApplicationDefinedPortIT { @LocalServerPort private Integer port; @LocalManagementPort private Integer managementPort; @Test void printPortsInUse() { System.out.println(port); // 8042 System.out.println(managementPort); // 9042 } } |
Running our integration tests with this configuration, we might clash with a running Spring Boot application. As only one process can occupy a port at any given time, we run into conflicts if that port is already in use.
As an alternative, we can define a new port of our integration test to avoid any clash. However, with this configuration, we won't be able to start multiple Spring TestContexts with @SpringBootTest
. Once we run the first integration test, all subsequent attempts to start the context will fail because the port is already acquired.
1 2 3 4 5 | Caused by: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'; nested exception is org.springframework.boot.web.server.PortInUseException: Port 8042 is already in use at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) |
Rarely Applicable for Integration Tests: WebEnvironment.NONE
As the name of the enum applies, Spring won't create a WebApplicationContext
and won't start the embedded servlet container for our test. This configuration isn't of big use when writing integration tests for our application, where Spring MVC controllers are the main entry point.
However, if our application has non-Spring MVC entry points, this configuration can help to test such parts. That's the case when our application processes incoming messages (e.g., JMS or SQS messages) or performs batch jobs.
Configure the Application Context and Environment
When using a Spring TestContext for our integration tests, we might need to configure the context and environments. This includes replacing or mocking beans and overriding application properties.
A pattern we'll see quite often is the usage of a integration-test
or a test profile in general. We can place a new property file (application-integration-test.yml
) inside src/test/resources
and define or override configuration values:
1 2 3 4 5 6 | spring: application: name: integration-test jpa: hibernate: ddl-auto: validate |
What's left is to activate this profile using@ActiveProfiles
on top of our test class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @SpringBootTest( webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "my.custom.property=inlined", "spring.main.banner-mode=off" // don't do this when Josh Long is pairing with you }) @ActiveProfiles("integration-test") class ApplicationConfigurationIT { @Autowired private Environment environment; @Test void shouldPrintConfigurationValues() { System.out.println(environment.getProperty("my.custom.property")); // inlined System.out.println(environment.getProperty("spring.application.name")); // integration-test } } |
In case we want to override a specific property only for a particular test, we can also inline properties using the properties
attribute of the @SpringBootTest
annotation.
When it comes to replacing Spring Beans with a mocked version, Spring Boot provides a convenient annotation for this:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @SpringBootTest class ApplicationConfigurationIT { @MockBean private CustomerService customerService; @Test void shouldRegisterUnknownUser() { Mockito.when(customerService.register("42")).thenReturn(7L); // test } } |
With the @MockBean
we replace the actual bean with a Mockito mock. In the example above, we're placing a mocked version of the CustomerService
into our TestContext. We can then instruct the behavior of this mock during test execution by using Mockito as we would use it for unit tests. To avoid confusion, it's important to understand the difference between @Mock and @MockBean.
If a Spring Bean is missing, or we want to replace it with another bean, there are various other strategies to fix no qualifying bean errors for Spring Boot tests.
When To Avoid Using @SpringBootTest
Equipped with this powerful test annotation, some Spring Boot newcomers try to use @SpringBootTest
for all their test cases. That's a bad practice for multiple reasons.
First, we would have to ensure our application context can start all the time. While this is easy to achieve when there are not external infrastructure components to integrate, this becomes a more complex topic as soon as we integrate a database, a message queue, or any other external systems (Testcontainers to the rescue).
Next, even though we only want to unit test a single class, we would need to start the entire context and use @MockBean
to mock all collaborators.
The following test class showcases this bad practice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // the same can be tested with a much faster unit test // don't do this @SpringBootTest class CustomerServiceTest { @Autowired private CustomerService cut; @MockBean private CustomerRepository customerRepository; @Test void shouldRegisterNewCustomer() { when(customerRepository.findByCustomerId("duke")).thenReturn(null); Long result = cut.register("duke"); assertEquals(42L, result); } } |
For the test case above, we're better off writing a good old unit test. This way, we only rely on JUnit and Mockito without any Spring context support.
In general, we should try to cover as much as possible with unit tests, use Spring Boot test slices where applicable, and only some integration tests.
Overusing @SpringBootTest while also creating multiple context configurations will lead to increased build times as Spring won't be able to reuse a cached Spring TestContext. Such integration tests are rather expensive.
Summary of Using @SpringBootTest for Integration Tests
In short, these should be the key takeaways for you:
@SpringBootTest
is a powerful tool to write integration tests- not every part of your application should be tested with this expensive test setup
- be aware that the default web environment is
MOCK
(no port and no Tomcat) - there are multiple strategies to tweak the context configuration and environment
- be aware of the increased build time when creating different Spring TestContext configurations
As a rule of thumb: Cover as much as possible with fast-running unit tests, use slice contexts where applicable, and ensure the overall functionality with some integration tests using @SpringBootTest
.
As part of the Testing Spring Boot Applications Masterclass, we're going to apply the learnings of this guide for testing a real-world Spring Boot application. We'll cover unit, integration, and end-to-end testing with straightforward explanations. This includes multiple strategies to provide external infrastructure components (e.g., database, messaging queues, etc.) for testing purposes. Save yourself some trial and error time when trying to test your Spring Boot application with this deep-dive testing course.
The source code for this guide to Spring Boot integration tests with @SpringBootTest is available on GitHub.
Joyful testing,
Philip
Is there any tool so that you quantify the amount of expensive between the annotations
@SpringBootTest
and junit@Test
and@WebMvcTest
so you can Even use junit@Test
for testing test endpoints end even mocks like you describe in the Paragraph „ when to avoid@SpringBootTes
t „ can you write an example of how to use unit test instead of// the same can be tested with a much faster unit test
// don't do this
@SpringBootTest
class CustomerServiceTest {
@Autowired
private CustomerService cut;
@MockBean
private CustomerRepository customerRepository;
@Test
void shouldRegisterNewCustomer() {
when(customerRepository.findByCustomerId("duke")).thenReturn(null);
Long result = cut.register("duke");
assertEquals(42L, result);
}
How would the unit test without mocks work instead can you give an example?
Hi Edddy,
great question. I’m not aware of any tool that can measure this but you can use the time information that Gradle/Maven outputs when running your tests. When comparing the examples it’s not a
@SpringBootTest
vs.@Test
vs.@WebMvcTest
because all tests use JUnit’s@Test
annotation. It’s rather a comparison between a test with Spring context support and a test without Spring context support. For any non-trivial application, starting the entire Spring context can take up to 20-30 seconds, and if most of the started TestContexts can’t be reused, that’s quite time expensive. A traditional unit test (no Spring context support, just JUnit & Mocito) usually finishes in milliseconds.Regarding the preferable example: The unit test would also use mocks, but no Spring context support. The test will look similar to what is described in the Mockito section here.
Kind regards,
Philip
Hello,
I must say that I am little bit about all the annotation.
Everywhere is mention WebApplicationContext and Spring MVC, but what about the REST api.
For me spring MVC deals with (Model, View and Controller), traditionally JSP pages. It is correct for example to use @WebMVCTest to test a REST controller?
Shouldn’t be there something like @WebRestTest ? And also a RestApplicationContext?
I can’t find any good resources to clarify this details.
Hi Adrian,
I had a similar confusion when I started with Spring.
With Spring Web MVC, you can write endpoints that either return a server-side rendered view (e.g. Thymeleaf or JSP) AND a REST API where you return data as the HTTP response body (e.g.
XML
orJSON
).You usually use
@Controller
for the server-side rendered views as the returnedString
of those methods represents the name of a view that should be rendered. Whereas with@RestController
, you write a REST API where the response type is rendered as part of the HTTP body.You test both endpoints with
@WebMvcTest
.Let me know if that clarifies it,
Philip