Once you’ve learned to write unit tests with JUnit + Mockito, the next hurdle is integration testing. “I want to verify behavior against a real database, not a mocked Service” — this is a common requirement in real-world projects.

This article walks through integration testing step by step, from the basics of booting up the full application with @SpringBootTest to connecting to a real database running in a Docker container using Testcontainers. It assumes you have already written unit tests with JUnit + Mockito. This article targets Spring Boot 3.1+ / Testcontainers 1.18+.

Difference Between @SpringBootTest and @WebMvcTest

First, let’s clarify the scope difference between these two annotations.

AnnotationStartup ScopeSpeed
@WebMvcTestWeb layer onlyFast
@SpringBootTestEntire applicationSlow

@WebMvcTest is a slice test that loads only the Controller — Service and Repository are mocked. @SpringBootTest starts the entire ApplicationContext, so use it when you want to verify the full chain from Controller → Service → Repository → DB.

Basic Setup for @SpringBootTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void ユーザー一覧が取得できる() {
        ResponseEntity<List> response = restTemplate.getForEntity("/api/users", List.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

The webEnvironment options include MOCK (default), RANDOM_PORT, DEFINED_PORT, and others. If you want to verify real HTTP requests, RANDOM_PORT is the most convenient choice.

Configuring application.yml for Tests

Place test-specific configuration in src/test/resources/application.yml. If using H2, you also need to add the dependency (com.h2database:h2 scope=test).

# src/test/resources/application.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop

H2 is sufficient for basic verification, but a situation where “production uses PostgreSQL but tests use H2” can cause you to miss bugs due to SQL dialect differences. For example, PostgreSQL-specific syntax like ON CONFLICT may not work in H2. This is where Testcontainers comes in.

What Is Testcontainers?

It’s a library that automatically starts and stops Docker containers during test execution. Since it runs with the real database engine, you can verify behavior including SQL dialects and database-specific quirks. Prerequisites: Docker is required in both local and CI environments.

Adding Dependencies

Since Spring Boot 3.1, Spring Boot’s BOM manages the Testcontainers version, so you can add it without specifying a version.

<!-- pom.xml -->
<dependencies>
  <dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

For Gradle:

testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testRuntimeOnly 'com.h2database:h2'

Example: @DataJpaTest + Testcontainers

If you only want to test the JPA layer, combining it with @DataJpaTest is convenient.

@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {

    @Container
    @ServiceConnection  // Spring Boot 3.1+: this alone auto-configures the DataSource
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @Autowired
    private UserRepository userRepository;

    @Test
    void ユーザーを保存して取得できる() {
        User user = new User("[email protected]", "Taro");
        userRepository.save(user);

        Optional<User> found = userRepository.findByEmail("[email protected]");
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Taro");
    }
}

@ServiceConnection automatically applies the container’s URL, username, and password to Spring’s configuration. Prior to 3.0, you had to manually register the URL, username, and password using @DynamicPropertySource.

@AutoConfigureTestDatabase(replace = NONE) is also required. By default, @DataJpaTest automatically switches to H2, so this annotation prevents that override.

End-to-End Testing with @SpringBootTest + Testcontainers

When you want to test the full chain from Controller → Service → Repository → real DB, combine it with @SpringBootTest.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class UserApiIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @AfterEach
    void tearDown() {
        userRepository.deleteAll();
    }

    @Test
    void ユーザーを登録してAPIで取得できる() {
        userRepository.save(new User("[email protected]", "Taro"));

        ResponseEntity<List> response = restTemplate.getForEntity("/api/users", List.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).hasSize(1);
    }
}

Caveats When Using @Transactional in Tests

With @DataJpaTest, applying @Transactional to test methods enables convenient automatic rollback. However, caution is needed in RANDOM_PORT environments.

Since the test code and server run on separate threads, the test-side transaction does not propagate to the server side. With @SpringBootTest(webEnvironment = RANDOM_PORT), it is more reliable to perform manual cleanup in @AfterEach. See also Spring Boot Transaction Management for more details.

Improving Test Speed: Context Reuse

Making the container a static field allows it to be reused across multiple test classes. Extracting it into a shared base class is the standard pattern.

// @Inherited means subclasses automatically inherit this (satisfied automatically under Testcontainers 1.17.3+ and Spring Boot 3.x BOM management)
@Testcontainers
abstract class IntegrationTestBase {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
}

Subclasses simply declare extends IntegrationTestBase. A single container is started and shared across all tests. However, when sharing a container across multiple classes, data left over from other classes can cause test failures — make sure to perform cleanup via @AfterEach in each test class.

Using @DirtiesContext causes the context to be recreated every time, so minimize its use.

In local development, setting testcontainers.reuse.enable=true in ~/.testcontainers.properties allows container reuse, but always disable this in CI environments.

Running Testcontainers in GitHub Actions

GitHub Actions’ Ubuntu runners come with Docker by default, so no special configuration is needed.

name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      TESTCONTAINERS_REUSE_ENABLE: false  # Disable reuse in CI
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Run tests
        run: ./mvnw test

There is no need to bring up a separate DB using a services: block. For Gradle, replace it with ./gradlew test. For containerization, also refer to Dockerizing a Spring Boot Application.

Summary

  • @WebMvcTest: When you want to test only the web layer quickly
  • @DataJpaTest + Testcontainers: When you want to verify the JPA layer against a real DB
  • @SpringBootTest + Testcontainers: When you need end-to-end integration tests from Controller through to DB

Making all tests @SpringBootTest will slow down your build. The key is to use it selectively for critical flows.

Once you adopt Testcontainers, you’ll be free from the “passes on H2 but fails on the production DB” problem. Once you get past the initial setup, it’s simple to use from there on out.