よくある光景:ローカルでは通るのにCIでは「真っ赤」になるテスト
インテグレーションテストを書いたことがある人なら、一度はこんな経験があるはずです。H2 Databaseを使用したローカル環境では100%パスするのに、本番環境と同じPostgreSQLにデプロイした途端、SQL構文エラーが発生する。あるいは、チームで共有のデータベースサーバーを使っているために、テストデータが干渉し合い、結果がめちゃくちゃになってしまう…。
6ヶ月以上、大規模プロジェクトでTestcontainersを導入してきた経験から、これが環境を統一する最善の方法だと確信しています。手動でのインストール作業は不要で、Testcontainersが必要なバージョンのDockerコンテナを自動的に起動してくれます。テストが終われば自動でクリーンアップされるため、不安定なテスト(Flaky tests)の発生率をほぼ0%に抑えることができました。
なぜDocker ComposeよりもTestcontainersの方が「価値がある」のか?
「Docker Composeで十分ではないか?」思う方もいるでしょう。その答えは「動的な制御能力」にあります。Testcontainersを使えば、JavaやGoのコード内でコンテナのライフサイクルを直接管理できます。
最大のメリットは、ランダムなポートマッピング機構です。もしローカルでPostgresが5432ポートで動いていても、Testcontainersは空いている別のポート(例:32768)を自動で見つけて新しいコンテナを起動します。これにより、ポートの競合を完全に回避できます。また、Ryukという「戦場掃除屋」のサイドカーコンテナが付属しており、テストプロセスが突然クラッシュしても、不要なコンテナを確実に破棄してリソースを解放してくれます。
Docker APIの設定中に長いJSONレスポンスを扱う際は、JSON Formatterを使って構成を素早く確認するのがおすすめです。生のコードを一行ずつ追うよりも、大幅に時間を短縮できます。
実践:PostgreSQLとRedisの統合
ここではSpring Bootプロジェクトでのセットアップ例を紹介します。他の言語でもロジックは同様です。
1. 必要なライブラリの追加
pom.xmlに依存関係を定義します。最適化された関数を利用するために、使用するデータベースの種類に応じた適切なモジュールを選択してください。
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
2. 動的なコンテナ構成
URLをハードコードしてはいけません。コンテナが正常に起動した後、Testcontainersに接続情報を返させます。プロジェクト全体で共有できるように、BaseIntegrationTestクラスを作成するのが一般的です。
@Testcontainers
public abstract class BaseIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("test_db")
.withUsername("user")
.withPassword("pass");
@Container
static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
}
}
@DynamicPropertySourceの仕組みが鍵となります。これにより、Dockerが割り当てた正確なポートを、ランタイム時にSpringアプリケーションへ注入できます。
3. テストケースの作成
この時点でのテスト作成は、本物のデータベースを操作するのと変わりません。ここでのテストがパスすれば、本番環境でも正しく動作すると確信を持てます。
class UserServiceTest extends BaseIntegrationTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldCreateUserSuccessfully() {
User user = new User("admin", "[email protected]");
userRepository.save(user);
assertNotNull(userRepository.findByUsername("admin"));
}
}
ビルドを遅延させないための3つの最適化手法
Testcontainersは非常に便利ですが、注意しないとCIのビルド時間が急増します。私が実践している最適化方法は以下の通りです。
- Singleton Containerの使用: テストクラスごとに新しいコンテナを起動しないでください。テストスイート全体で単一のスタティックなコンテナを使い回しましょう。あるプロジェクトでは、これによりビルド時間を15分から6分に短縮できました。
- Alpineイメージを優先: フル版ではなく、
postgres:15-alpineのような軽量版を常に使用してください。イメージサイズが小さいほどCIサーバーでのプルが速くなり、帯域幅とディスク容量を節約できます。 - Dockerへの十分なリソース割り当て: Postgres、Redis、Kafkaを同時に動かす場合は、Docker Desktopに少なくとも4GBのRAMを割り当ててください。メモリ不足は、コンテナの起動遅延やクラッシュの主な原因になります。
Lời kết
Testcontainersは単なるツールではなく、テスト環境を完全に制御するという考え方そのものです。環境の差異に悩む時間を費やす代わりに、コードで環境を定義しましょう。Jenkinsでの動的ポート設定や接続で困っていることがあれば、ぜひコメント欄で質問してください。一緒に解決しましょう。
