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.
| Annotation | Startup Scope | Speed |
|---|---|---|
@WebMvcTest | Web layer only | Fast |
@SpringBootTest | Entire application | Slow |
@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.