Integration tests verify the work of components together (with a real database, external services).
Simple analogy: A unit test is like checking an individual engine part (piston, valve). An integration test is like starting the engine and verifying the wheels spin. You are testing how parts work together, not in isolation.
Why it matters: Unit tests prove individual methods work correctly. But bugs often hide at the boundaries — between the controller and the service, between the service and the repository, between the application and the database. Integration tests catch these boundary issues: wrong SQL, missing transaction boundaries, incorrect JSON serialization, and failed dependency injection.
@SpringBootTest (full context):
1@SpringBootTest2@AutoConfigureMockMvc3class UserControllerIntegrationTest {45 @Autowired MockMvc mockMvc;6 @Autowired UserRepository userRepository;78 @BeforeEach9 void setup() {10 userRepository.deleteAll();11 }1213 @Test14 @Transactional // Rollback after test15 void shouldCreateUser() throws Exception {16 mockMvc.perform(post("/api/users")17 .contentType(MediaType.APPLICATION_JSON)18 .content("{\"name\":\"Alice\",\"email\":\"a@b.com\"}"))19 .andExpect(status().isCreated())20 .andExpect(jsonPath("$.name").value("Alice"))21 .andExpect(jsonPath("$.id").isNumber());22 }2324 @Test25 void shouldReturn404ForNonExistentUser() throws Exception {26 mockMvc.perform(get("/api/users/9999"))27 .andExpect(status().isNotFound());28 }29}
@WebMvcTest (web layer only — fast):
1@WebMvcTest(UserController.class)2class UserControllerWebTest {3 @Autowired MockMvc mockMvc;4 @MockBean UserService userService; // Only the service is mocked56 @Test7 void shouldReturnUser() throws Exception {8 when(userService.findById(1L)).thenReturn(new UserDto(1L, "Alice"));9 mockMvc.perform(get("/api/users/1"))10 .andExpect(status().isOk())11 .andExpect(jsonPath("$.name").value("Alice"));12 }1314 @Test15 void shouldReturn400ForInvalidInput() throws Exception {16 mockMvc.perform(post("/api/users")17 .contentType(MediaType.APPLICATION_JSON)18 .content("{\"name\":\"\",\"email\":\"bad\"}"))19 .andExpect(status().isBadRequest());20 }21}
@DataJpaTest (JPA layer only — with test database):
1@DataJpaTest2class UserRepositoryTest {3 @Autowired TestEntityManager entityManager;4 @Autowired UserRepository userRepository;56 @Test7 void shouldFindByEmail() {8 entityManager.persistAndFlush(new User("Alice", "a@b.com"));9 Optional<User> found = userRepository.findByEmail("a@b.com");10 assertTrue(found.isPresent());11 assertEquals("Alice", found.get().getName());12 }1314 @Test15 void shouldReturnEmptyForUnknownEmail() {16 Optional<User> found = userRepository.findByEmail("nobody@test.com");17 assertTrue(found.isEmpty());18 }19}
@TestConfiguration — additional beans for tests:
1@TestConfiguration2public class TestConfig {3 @Bean4 public Clock clock() {5 return Clock.fixed(Instant.parse("2024-01-15T12:00:00Z"), ZoneId.of("UTC"));6 }7}
Testcontainers (real databases in Docker):
1@Testcontainers2@SpringBootTest3class UserRepositoryIntegrationTest {4 @Container5 static PostgreSQLContainer<?> postgres =6 new PostgreSQLContainer<>("postgres:15");78 @DynamicPropertySource9 static void configure(DynamicPropertyRegistry registry) {10 registry.add("spring.datasource.url", postgres::getJdbcUrl);11 registry.add("spring.datasource.username", postgres::getUsername);12 registry.add("spring.datasource.password", postgres::getPassword);13 }14}
Common mistakes:
@SpringBootTest when @WebMvcTest suffices — full context tests are 5-10x slower.@Transactional or @BeforeEach cleanup.