Integration Testing — Testcontainers, @SpringBootTest, and Embedded Databases
In this tutorial, you will learn about Integration Testing. We cover key concepts, practical examples, and best practices to help you master this topic.
Integration testing validates the interaction between components in a running application context, using Testcontainers for real database testing and @SpringBootTest for Spring Boot integration tests. While unit tests verify individual classes in isolation, integration tests verify that the pieces work together.
What You'll Learn
- @SpringBootTest for loading the application context
- Testcontainers for Docker-backed database testing
- Embedded databases (H2) for lightweight testing
- @DataJpaTest for repository-level testing
- @WebMvcTest for controller-level testing
Why It Matters
Integration tests catch real bugs that unit tests miss — a query that works against H2 but fails on PostgreSQL, a transaction boundary that does not commit correctly, or a serialization mismatch.
Real-World Use
CI pipelines run integration tests against Testcontainers-managed databases. Spring Boot applications use @SpringBootTest for end-to-end API testing.
Testcontainers
Testcontainers provides disposable Docker containers for integration tests:
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User("Alice", "alice@example.com");
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("alice@example.com");
assertTrue(found.isPresent());
assertEquals("Alice", found.get().getName());
}
}
Advantages over H2
- Tests run against the same database as production (PostgreSQL, MySQL)
- Catches database-specific SQL dialect issues
- Verifies Migration scripts work correctly
@SpringBootTest
Loads the full application context:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateAndReturnUser() {
User user = new User("Alice", "alice@example.com");
ResponseEntity<User> createResponse = restTemplate.postForEntity(
"/api/users", user, User.class);
assertEquals(201, createResponse.getStatusCodeValue());
ResponseEntity<User> getResponse = restTemplate.getForEntity(
"/api/users/" + createResponse.getBody().getId(), User.class);
assertEquals("Alice", getResponse.getBody().getName());
}
}
WebEnvironment Options
| Mode | Description |
|---|---|
| MOCK | Mock servlet environment (no real server) |
| RANDOM_PORT | Real server on random port |
| DEFINED_PORT | Real server on configured port |
| NONE | No web environment |
@DataJpaTest
Loads only JPA-related beans:
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BookRepositoryTest {
@Autowired
private BookRepository bookRepository;
@Test
void shouldFindBooksByAuthor() {
Book book = new Book("Java Basics", "Alice");
bookRepository.save(book);
List<Book> found = bookRepository.findByAuthor("Alice");
assertEquals(1, found.size());
}
}
@WebMvcTest
Loads only web layer components:
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
when(userService.findById(1L)).thenReturn(new User("Alice"));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"));
}
}
Embedded Databases (H2)
For lightweight testing without Docker:
@TestConfiguration
static class TestDatabaseConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
}
Common Mistakes
- Not cleaning test data between tests. Use @DirtiesContext or truncate tables in @BeforeEach.
- Sharing Testcontainers incorrectly. @Container (static) shares across all tests; instance creates per test.
- Forgetting @DynamicPropertySource. Testcontainers uses random ports; override datasource URL dynamically.
- Using @SpringBootTest when a slice test would suffice. Use @DataJpaTest or @WebMvcTest for faster tests.
- Running integration tests without Docker. Testcontainers requires a Docker runtime.
Practice Questions
1. What is the difference between @SpringBootTest and @DataJpaTest? @SpringBootTest loads the full context. @DataJpaTest loads only JPA beans (faster).
2. Why use Testcontainers instead of H2? H2 has a different SQL dialect than production databases. Testcontainers runs the same database as production.
3. What does @DynamicPropertySource do? It overrides Spring properties at runtime, typically for Testcontainers connection info.
4. What is the purpose of MockMvc? It simulates HTTP requests and verifies responses without starting a real server.
5. How do you prevent test pollution? Use @BeforeEach cleanup, @Transactional for rollback, or @DirtiesContext to reload context.
Challenge Question: Write an integration test for a REST API managing books. Use Testcontainers with PostgreSQL, @SpringBootTest with RANDOM_PORT, and TestRestTemplate. Test CRUD operations and verify HTTP status codes and response bodies.
FAQ
Mini Project
Write a complete integration test suite for a Library API:
- Create a Spring Boot app with Book entity, JPA repository, REST controller
- Write @DataJpaTest for the repository
- Write @WebMvcTest for the controller with mocked service
- Write @SpringBootTest with Testcontainers for end-to-end test
- Verify that books can be created, retrieved, searched, and deleted
- Test error cases: not found, validation errors, conflict
What's Next
Integration tests validate system behavior. But production systems also need Observability. Lesson 50 covers logging — SLF4J, Logback, Log4j2, MDC for contextual logging, structured logging, and log levels for different environments.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro