Skip to content

Integration Testing — Testcontainers, @SpringBootTest, and Embedded Databases

DodaTech Updated 2026-06-28 4 min read

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

  1. Not cleaning test data between tests. Use @DirtiesContext or truncate tables in @BeforeEach.
  2. Sharing Testcontainers incorrectly. @Container (static) shares across all tests; instance creates per test.
  3. Forgetting @DynamicPropertySource. Testcontainers uses random ports; override datasource URL dynamically.
  4. Using @SpringBootTest when a slice test would suffice. Use @DataJpaTest or @WebMvcTest for faster tests.
  5. 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

What is the difference between integration and unit tests?

Unit tests test one class in isolation using mocks. Integration tests test multiple components together — service with repository, controller with service, etc.

How do I speed up @SpringBootTest?

Use slice tests (@DataJpaTest, @WebMvcTest) instead of full context. Use @ContextConfiguration with minimal config. Enable lazy initialization.

Can I use Testcontainers in CI without Docker?

GitHub Actions and most CI providers support Docker. For environments without Docker, use H2 as a fallback with profile-specific configuration.

What is the difference between @MockBean and @Mock?

@Mock is Mockito's annotation (creates a mock). @MockBean is Spring Boot's annotation (creates a mock and replaces the bean in the application context).

How do I test database transactions?

Use @Transactional on the test method — Spring rolls back changes after the test. For commit-verification tests, use @Commit to persist changes.

Mini Project

Write a complete integration test suite for a Library API:

  1. Create a Spring Boot app with Book entity, JPA repository, REST controller
  2. Write @DataJpaTest for the repository
  3. Write @WebMvcTest for the controller with mocked service
  4. Write @SpringBootTest with Testcontainers for end-to-end test
  5. Verify that books can be created, retrieved, searched, and deleted
  6. 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