Date: December 8, 2024
Integration testing is crucial for ensuring your Spring Boot application’s different layers work seamlessly together — especially the web (REST controllers) and data (repositories, databases) layers. Unlike unit tests, integration tests load parts of the Spring context and often interact with real or in-memory databases to validate your app behavior end-to-end.
In this post, we’ll explore strategies and best practices to write effective integration tests for Spring Web and Spring Data.
Why Integration Tests?
- Verify that web endpoints, service layers, and persistence layers collaborate correctly.
- Catch issues missed by isolated unit tests (e.g., incorrect query, transaction problems).
- Provide confidence before deploying to production.
Tooling Overview
Spring Boot provides powerful support for integration testing via:
- @SpringBootTest: Loads full Spring context.
- MockMvc: Simulates HTTP requests for testing controllers.
- @DataJpaTest: Loads only JPA-related components, ideal for repository testing.
- Testcontainers: Run lightweight, throwaway Docker containers (e.g., PostgreSQL) for real database tests.
- @Transactional: Roll back transactions after each test to keep DB clean.
Strategy 1: Testing Controllers with MockMvc and In-Memory DB
Example: Test a REST API backed by H2 in-memory database.
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
@Transactional
public class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setup() {
userRepository.save(new User(null, "Bob"));
}
@Test
void testGetUser() throws Exception {
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Bob"));
}
}
Notes:
@Transactional
rolls back DB changes after each test.application-test.properties
configures the app to use H2 database.- MockMvc performs requests without running a real HTTP server.
Strategy 2: Isolating Repository Tests with @DataJpaTest
For repository testing focusing on JPA logic:
@DataJpaTest
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void testFindByName() {
userRepository.save(new User(null, "Alice"));
Optional<User> user = userRepository.findByName("Alice");
Assertions.assertTrue(user.isPresent());
Assertions.assertEquals("Alice", user.get().getName());
}
}
This annotation configures an embedded database, scans for JPA entities, and sets up Spring Data repositories for fast, focused tests.
Strategy 3: Using Testcontainers for Realistic DB Testing
While in-memory DBs are fast, sometimes you want to test with your actual database engine.
@Testcontainers
@SpringBootTest
public class UserRepositoryTest {
@Container
public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private UserRepository userRepository;
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void testFindAllUsers() {
userRepository.save(new User(null, "Eve"));
List<User> users = userRepository.findAll();
Assertions.assertFalse(users.isEmpty());
}
}
Advantages:
- Runs your tests against a real PostgreSQL container.
- Tests queries and DB-specific features exactly as in production.
- Works well in CI pipelines.
Strategy 4: Full End-to-End Tests with WebTestClient
For reactive apps or to test actual HTTP endpoints, use WebTestClient:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ReactiveUserControllerTest {
@Autowired
private WebTestClient webTestClient;
@Test
void testGetUsers() {
webTestClient.get().uri("/users")
.exchange()
.expectStatus().isOk()
.expectBodyList(User.class)
.hasSizeGreaterThan(0);
}
}
Best Practices Summary
- Use in-memory databases for fast feedback in most tests.
- Apply Testcontainers for realistic DB integration in critical paths.
- Use @Transactional to keep tests isolated.
- Mock external services when needed with tools like WireMock.
- Combine unit tests (fast, isolated) with integration tests (context and real DB).
- Run integration tests regularly in your CI pipeline.
Conclusion
Integration testing with Spring Web and Data is essential for delivering reliable apps. By strategically choosing testing scope, tools, and data sources, you can catch issues early and maintain high quality.