Spring Bootで結合テストを書く方法 - @SpringBootTestとTestcontainersでDBまで通したテスト
JUnit + Mockitoを使った単体テストが書けるようになると、次の壁が結合テストですよね。「MockしたServiceじゃなく、本物のDBに接続した状態で動くか確かめたい」というのは実務でよく出てくる要求です。
この記事では @SpringBootTest でアプリ全体を起動するテストの基本から、 Testcontainers を使ってDockerコンテナ上の実DBに接続する結合テストまでをステップごとに解説します。JUnit + Mockitoの単体テストを書いたことがある前提で進めます。本記事は Spring Boot 3.1以降 / Testcontainers 1.18以降 を対象としています。
@SpringBootTestと@WebMvcTestの違い
まず整理しておきたいのが2つのアノテーションのスコープの違いです。
| アノテーション | 起動スコープ | 速度 |
|---|---|---|
@WebMvcTest | Web層のみ | 速い |
@SpringBootTest | アプリ全体 | 遅い |
@WebMvcTest はControllerだけをロードするスライステストで、ServiceやRepositoryはMockになります。@SpringBootTest はアプリ全体の ApplicationContext を起動するので、Controller→Service→Repository→DBまで通して確認したいときはこちらです。
@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);
}
}
webEnvironment の選択肢は MOCK(デフォルト)、RANDOM_PORT、DEFINED_PORT などがあります。実HTTPリクエストを確認したいなら RANDOM_PORT が使いやすいです。
テスト用application.ymlの設定
src/test/resources/application.yml にテスト専用の設定を置きます。H2を使う場合は依存の追加も必要です(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で十分ですが、「本番はPostgreSQLなのにテストはH2」という状況はSQL方言の差でバグを見逃します。例えばPostgreSQL固有の ON CONFLICT 構文はH2では動作しないことがあります。そこでTestcontainersの出番です。
Testcontainersとは
テスト実行中にDockerコンテナを自動起動・終了してくれるライブラリです。本物のDBエンジンで動くので、SQL方言やDB固有の挙動も含めて確認できます。 前提条件 としてローカルとCI環境にDockerが必要です。
依存関係の追加
Spring Boot 3.1以降はSpring BootのBOMがTestcontainersのバージョンを管理するので、バージョン指定なしで追加できます。
<!-- 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>
Gradleはこちらです。
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testRuntimeOnly 'com.h2database:h2'
@DataJpaTest + Testcontainersの例
JPA層だけをテストしたいなら @DataJpaTest との組み合わせが便利です。
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {
@Container
@ServiceConnection // Spring Boot 3.1以降: これだけで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 はコンテナのURL・ユーザー名・パスワードを自動でSpringの設定に反映してくれます。3.0以前は @DynamicPropertySource でURL・ユーザー名・パスワードを手動登録する必要がありました。
@AutoConfigureTestDatabase(replace = NONE) も必須です。デフォルトだと @DataJpaTest が自動でH2に切り替えてしまうので、これを付けて上書きを防ぎます。
@SpringBootTest + Testcontainersでエンドツーエンドテスト
Controller→Service→Repository→実DBまで通したい場合は @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);
}
}
@Transactionalをテストに付けるときの注意点
@DataJpaTest ではテストメソッドへの @Transactional 付与で自動ロールバックが便利です。ただし RANDOM_PORT 環境では注意が必要です。
テストコードとサーバー側が 別スレッド で動くため、テスト側のトランザクションがサーバー側に伝播しません。@SpringBootTest(webEnvironment = RANDOM_PORT) では @AfterEach で手動クリーンアップするほうが確実です。詳細はSpring Bootのトランザクション管理も参照してください。
テスト速度改善:コンテキスト再利用
コンテナを static フィールドにすると複数テストクラス間で再利用できます。共通の基底クラスに切り出すのが定番パターンです。
// @Inheritedなのでサブクラスへ自動継承(Testcontainers 1.17.3以降、Spring Boot 3.x BOM管理下では自動的に満たされます)
@Testcontainers
abstract class IntegrationTestBase {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
}
サブクラスは extends IntegrationTestBase を宣言するだけです。コンテナは1つだけ起動し、テスト全体で共有されます。ただし複数クラスでコンテナを共有する場合、各テストクラスで @AfterEach によるクリーンアップを行わないと他クラスのデータが残留して失敗することがあります。
@DirtiesContext を使うとコンテキストが毎回再作成されるので、使用は最小限にとどめましょう。
ローカル開発では ~/.testcontainers.properties に testcontainers.reuse.enable=true を設定するとコンテナを使い回せますが、 CI環境では必ず無効 にしてください。
GitHub ActionsでTestcontainersを動かす
GitHub ActionsのubuntuランナーにはDockerがデフォルトで入っているので、特別な設定なしに動きます。
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
env:
TESTCONTAINERS_REUSE_ENABLE: false # CI環境でreuse無効化
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Run tests
run: ./mvnw test
services: ブロックでDBを別途立ち上げる必要はありません。Gradleなら ./gradlew test に変えてください。Dockerコンテナ化についてはSpring BootアプリのDockerコンテナ化も参考にしてください。
まとめ
@WebMvcTest:Web層だけを高速にテストしたいとき@DataJpaTest+ Testcontainers:JPA層を実DBで確認したいとき@SpringBootTest+ Testcontainers:Controller→DBまで通した結合テストが必要なとき
全テストを @SpringBootTest にしてしまうとビルドが遅くなります。重要なフローに絞って使うのがコツです。
Testcontainersを一度導入すると「H2ではパスするのに本番DBで落ちる」問題から解放されます。最初のセットアップだけ乗り越えれば、あとはシンプルに使えますよ。