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つのアノテーションのスコープの違いです。

アノテーション起動スコープ速度
@WebMvcTestWeb層のみ速い
@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_PORTDEFINED_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.propertiestestcontainers.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で落ちる」問題から解放されます。最初のセットアップだけ乗り越えれば、あとはシンプルに使えますよ。