Spring Boot Integration Testing With @SpringBootTest
In this article, we look at how to write integration tests with @SpringBootTest
.
First, we will discuss different types of integration tests. Then, we will discover customization options for @SpringBootTest
and how to write system tests with an embedded web server running.
Related Video
If you prefer learning from videos, make sure to check out the accompanying video in YouTube.What Is an Integration Test?
Before we go any further, let’s define what we mean by integration testing. There are two different notions of what constitutes an integration test:
- Narrow integration tests that exercise only part of the application and use test doubles for some components or external services. Some call these component tests or service tests to make the distinction.
- Broad integration tests that need the whole application running and exercise the application through UI or network calls. Some call these system tests or end-to-end tests to make the distinction.
The Spring Boot test slices like @WebMvcTest
or @DataJpaTest
that we saw earlier are narrow integration tests. They only load part of the application context and allow mocking of unneeded dependencies. Also, the tests that we wrote with WebClient
and MockWebServer
are narrow integration tests because they test a smaller slice of the application but communicate over HTTP to a local mock server.
So why does this matter? Well, the truth is, it doesn’t matter.
However, it’s good to acknowledge that the software development community hasn’t settled on well-defined terminology. What we mean by a unit test or an integration test might mean something else for someone else.
What does matter is that we don’t focus only on the broad integration tests. Using a narrower scope for integration tests makes them faster, easier to write, and more resilient.
We should use broader tests to give us confidence that our application works correctly. However, we shouldn’t test conditional logic or edge cases in those tests. Make sure broader tests only cover what narrower tests couldn’t cover.
The above doesn’t mean that we should only write unit tests that mock everything. We should use mocks sparingly and only mock things like the file system, database, or network connection.
Write an Integration Test With a Mock Environment
We’ll start by writing an integration test that loads the entire Spring application context but configures MockMvc
to perform requests and responses. A test like this has a broader scope than the Spring test slices like @WebMvcTest
or @DataJpaTest
but is not starting an embedded server:
@SpringBootTest
@AutoConfigureMockMvc
public class MockEnvIntegrationTests {
@Autowired
private MockMvc mockMvc;
@Test
void createOrder() throws Exception {
mockMvc.perform(post("/order")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"amount\": \"EUR100.0\"}"))
.andExpect(status().isCreated());
}
}
From the perspective of writing the test, this looks similar to the @MockMvcTest
that we saw before. However, a crucial difference here is that @MockMvcTest
only configures a part of the application context while @SpringBootTest
loads the entire one.
Spring Boot has several auto configurations that configure smaller parts of the context. Here we are using @AutoConfigureMockMvc
that is not included in @SpringBootTest
but is part of @WebMvcTest
. The Spring Boot test slices constitute of multiple auto configurations like this one.
A typical mistake is to add assertions for things like the response contents in these broader tests. If we already have a @MockMvcTest
that tests the same thing, there is no need to do it here.
Conversely, if a broader test detects an error and there’s no narrower test failing, we should try to write a narrower test for it.
Use Custom Properties with @TestPropertySource
@SpringBootTest
sets up an in-memory database for tests by default. To override some of the Spring properties, we can use the @TestPropertySource
annotation:
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:tc:postgresql:13.2-alpine://payment"
})
public class MockEnvIntegrationTests {
// ...
}
Now the test fires up a Testcontainers Docker container with PostgreSQL running and runs the tests against that.
We could override any other Spring properties with the annotation too. However, it’s good to keep in mind that we should try to keep the test environment as close to the actual implementation as possible. Adding more customizations to the tests makes them different from the real application.
Move Properties To a Profile With @ActiveProfiles
We might have a lot of tests that want to override the same properties. In such a case, instead of just using @TestPropertySource
we can externalize the configuration using @ActiveProfiles
.
We start by adding the properties into a file called application-test.yml
:
spring:
datasource:
url: jdbc:tc:postgresql:13.2-alpine://payment
Now we can refer to a Spring profile called test
in our tests by using the @ActiveProfiles("test")
annotation:
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class MockEnvIntegrationTests {
// ...
}
Spring will now read the properties from the application-test.yml
file directly, and we can reuse the configuration between any tests that require them.
Roll Back Changes Using @Transactional
Earlier, when testing the persistence layer we saw how @DataJpaTest
makes tests @Transactional
by default. However, @SpringBootTest
does not do this, so if we would like to roll back any changes after tests, we have to add the @Transcational
annotation ourselves:
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@ActiveProfiles("test")
public class MockEnvIntegrationTests {
@Autowired
private MockMvc mockMvc;
@Test
@Sql("/unpaid-order.sql")
void payOrder() throws Exception {
mockMvc.perform(post("/order/{id}/payment", 1)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"creditCardNumber\": \"4532756279624064\"}"))
.andExpect(status().isCreated());
}
}
We can also use @Sql
annotation to insert any required data before the tests.
Tests should be independent to run without other tests, and their results shouldn’t affect any other tests. This independence is significant in broader tests that load a larger chunk of the application context and potentially share things.
Mock an External Service With @MockBean
Sometimes our application might call external services that we don’t want to call in our tests. In the previous article, we used MockWebServer
to start up a local mock server for our tests. Since we also wrote a wrapper class for the WebClient
making those external requests, we can now mock the wrapper class using the @MockBean
annotation:
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@ActiveProfiles("test")
public class MockEnvIntegrationTests {
@Autowired
private MockMvc mockMvc;
@MockBean
private ExchangeRateClient exchangeRateClient;
@Test
@Sql("/paid-order.sql")
void getReceipt() throws Exception {
CurrencyUnit eur = Monetary.getCurrency("EUR");
CurrencyUnit usd = Monetary.getCurrency("USD");
when(exchangeRateClient.getExchangeRate(eur, usd))
.thenReturn(BigDecimal.valueOf(0.8412));
mockMvc.perform(get("/order/{id}/receipt?currency=USD", 1))
.andExpect(status().isOk());
}
}
Mocking the client allows us to use Mockito to return responses from our wrapper class. Of course, this is now skipping making real REST requests through HTTP.
To gain more confidence over the web client working in the real application, we’d still want to use a mock web server for our tests. So, we’ll take a look at how to configure MockWebServer
together with @SpringBootTest
in a bit.
Write an End-To-End Test With a Running Server
So far, we have been only looking at a test running with a mock environment. To provide a real web environment, we can tell Spring Boot to do that:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class ServerIntegrationTests {
@Autowired
private WebTestClient webClient;
// ...
}
Now Spring starts an embedded web server listening on a random port. Since we cannot use MockMvc
anymore, we can autowire WebTestClient
instead. Spring Boot will automatically configure the client so that it makes requests to the embedded web server.
Now we can write integration tests that make actual HTTP requests using the WebTestClient
fluent API:
@Test
void createOrder() {
webClient.post().uri("/order")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{\"amount\": \"EUR100.0\"}")
.exchange()
.expectStatus().isCreated();
}
The approach is very similar to what we saw with MockMvc
but now also involves the actual HTTP stack in the tests.
Clean Up Test Data
When we start the embedded web server in our tests, the server and the client run in separate threads. Therefore, if we start a transaction in the test, it’s not the same transaction as on the webserver. This separation means that we cannot use @Transactional
in our tests anymore because we cannot roll back a transaction in the server thread.
The solution to this inconvenience is to insert and delete data manually:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class ServerIntegrationTests {
@Autowired
private WebTestClient webClient;
@Autowired
private OrderRepository orderRepository;
@AfterEach
void deleteEntities() {
orderRepository.deleteAll();
}
@Test
void payOrder() {
Order order = new Order(LocalDateTime.now(), BigDecimal.valueOf(100.0), false);
Long orderId = orderRepository.save(order).getId();
webClient.post().uri("/order/{id}/payment", orderId)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{\"creditCardNumber\": \"4532756279624064\"}")
.exchange()
.expectStatus().isCreated();
}
}
In our test, we first insert the entities required for the test. Then, after the test, we make sure to clean up using the JUnit 5 @AfterEach
annotation and delete all entities from the database.
Use @DynamicPropertySource to Mock an External Service
Previously, we used @MockBean
to mock the web client calls to an external service in the example with the mock environment. However, if we want to test the complete end-to-end chain, we don’t want to do that.
Our tests manually passed the mock webserver URL to the web client wrapper class in a previous article. Now that we are loading up the application context in our @SpringBootTest
we cannot do that. We also cannot use @TestPropertySource
either because we don’t know the mock web server address before starting it in the test.
For such case, we can use the @DynamicPropertySource
annotation to register dynamic properties:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class ServerIntegrationTests {
// ...
private static MockWebServer mockWebServer;
@BeforeAll
static void setupMockWebServer() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
}
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("exchange-rate-api.base-url",
() -> mockWebServer.url("/").url().toString());
}
}
This way, we can first start a MockWebServer
instance in the test and tell the server URL to Spring Boot via DynamicPropertyRegistry
. Now we can use the MockWebServer
in our tests:
@Test
void getReceipt() {
Order order = new Order(LocalDateTime.now(), BigDecimal.valueOf(100.0), true);
Payment payment = new Payment(order, "4532756279624064");
Long orderId = orderRepository.save(order).getId();
paymentRepository.save(payment);
mockWebServer.enqueue(
new MockResponse().setResponseCode(200)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("{\"conversion_rate\": 0.8412}")
);
webClient.get().uri("/order/{id}/receipt?currency=USD", orderId)
.exchange()
.expectStatus().isOk();
}
It’s good to understand that we are only communicating with the server via REST calls through the HTTP connection. So we are looking at the application from the outside.
Understand Application Context Caching
Spring’s test framework caches application context between tests. This mechanism means that if subsequent tests use the same configuration, they start faster because they can use the already loaded application context.
If different tests need different configurations, Spring Boot cannot cache the application context and loads a new context with that configuration. So whenever we use @MockBean
, @ActiveProfiles
, @DynamicPropertySource
or any other annotations that customize the configuration, Spring creates a new application context for the tests.
A common mistake with Spring Boot integration tests is to start every test with @SpringBootTest
and then try to configure each test for a specific case. This approach usually ends up in a very slow test suite because Spring Boot cannot cache the application contexts used in the tests.
It also ends up with test configurations that are much more complex than necessary. A better approach is to try to stay with the Spring Boot pre-configured test slices like @WebMvcTest
and @DataJpaTest
as much as possible. For broader integration tests it’s better to try to write a single configuration for any tests using @SpringBootTest
.
Summary
Whether we call a test a unit test or an integration test is not important. What is important is that we try to test on as narrow a scope as possible without testing the implementation but the behavior. To gain confidence on a broader scope, we only test things that the narrower scope didn’t cover.
Spring Boot provides test slice configurations for narrow integration tests. To write broader integration tests, we can use the @SpringBootTest
annotation. There are plenty of options to customize the application context in Spring Boot tests, but we should use them cautiously. It’s best to try to stick with the test slices and have a single configuration for the broader integration tests.
You can find the example code for this article on GitHub.