Testcontainersを使う時に気を付けるポイント考えてみた

# はじめに
最近TestcontainersというOSSの存在を知り、試しに触ってみたところDB接続のあるアプリでローカル環境にDBを用意しなくてもすこぶる簡単にDBコンテナを起動できました。これは環境構築が楽になるし、開発・テストする上で嬉しいこと結構ありそうだなと思い記事にします。
使い方は公式のクイックスタートや分かりやすい記事が結構あったのであまり触れずに、実際にプロジェクトとかで使うならこの辺気を付けた方がいいなというところを見て行きたいと思います。
# Testcontainersとは
データベースやWebブラウザなどDockerコンテナで実行できるものをほぼすべて使い捨ての軽量インスタンスとして提供してくれるオープンソース・ライブラリです。
Testcontainersがサポートしている開発言語、お気に入りのIDE、そしてDocker環境があればすぐに始められます。

:::note info
Docker環境については以下のいずれかを用意する必要があります。
- DockerDesktop
- LinuxであればDockerEngine
- [Testcontainers Cloud]()
:::

SpringBootでTestcontainersがサポートされるようになり、Spring initializrでも依存関係として選択できるので、自分でpomやbuild.graldeに依存関係を追加しなくても簡単に始められます。（実際すぐでした）

# Testcontainersを触ってみる
ベストプラクティスの話をする前にまずはSpringBootを使ってTestcontainersを触ってみたいと思います。
## Spring initialzrでSpringBootアプリの雛型を作成
Testcontainersを使ってPostgreSQLコンテナを起動したいと思います。
下の画像のようにDependenciesにTestcontainersとPostgreSQL Driverを入れてもらえれば他はお好きなものを選んでください。
![スクリーンショット 2024-12-10 23.06.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336716/986b801c-4494-70f9-6259-886e5e5468d9.png)
## ソースを見てみましょう
以下のコードがすでに作成されています。
``` java
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {

	@Bean
	@ServiceConnection
	PostgreSQLContainer<?> postgresContainer() {
		return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
	}

}
```
`PostgreSQLContainer`モジュールのように、よく利用されるインフラストラクチャは以下のようにモジュールとして用意されています。

https://testcontainers.com/modules/

ちなみにモジュールが用意されていないものも`GenericContainer`というモジュールが用意されているので、問題なく使えます。

``` java
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Import(TestcontainersConfiguration.class)
@SpringBootTest
class TestcontainersDemoApplicationTests {

	@Test
	void contextLoads() {
	}

}
```
## アプリ実行してみる
![スクリーンショット 2024-12-13 7.09.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336716/3a8aa699-6297-38c9-4a98-f0b5824f059a.png)

![スクリーンショット 2024-12-13 7.11.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336716/af61edb0-1183-05b2-15bd-9536b569bcc2.png)
ということで特にソース書かなくてもPostgreSQLを起動することができました。



# 気を付けるポイントについて
それでは各種ドキュメントやガイドを見て、Testcontainersを使う上で気を付けた方がよいポイントをいくつか見て行きます。
## ポートの固定
Testcontainersはポートの動的マッピングを備えていて、コンテナ起動時にホスト側の利用可能なポートと起動するコンテナのポートを自動でマッピングします。
ポートを固定していると以下のようなケースがあるのでチームで開発/テストするのであれば、ポートは固定しない方がよいでしょう。
* メンバーによっては固定ポートを別のプロセスで実行している可能性がある
* 複数パイプラインを並行して起動している時は同じポートで複数のコンテナを起動してポートの競合が起きる

ただし、ローカル開発で利用する分には固定ポートを利用した方が便利です。
DBクライアントツールなど利用する時に、固定ポートにしていないとコンテナを起動するたびに接続情報を修正しなくてはいけないです。

## コンテナバージョンの指定
Spring initializrで作成した雛型アプリにはデフォルトで以下のようなソースがありました。
```java
PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
```
これは新しいDockerイメージがリリースされると運用中の実行環境とのバージョンに差異が出てしまうので、`latest`はなるべく使わない方がよいでしょう。
## コンテナのライフサイクル
複数のテストを同時実行した時などにそれぞれのテストでコンテナを起動しようとして失敗する可能性があります。
公式では以下2パターンのライフサイクル管理方法が案内されています。
* [JUnit5の拡張アノテーションを利用する方法](https://testcontainers.com/guides/testcontainers-container-lifecycle/#_using_junit_5_extension_annotations)
* [シングルトンコンテナパターンを利用する方法](https://testcontainers.com/guides/testcontainers-container-lifecycle/#_using_singleton_containers)

### JUnit5の拡張アノテーションを利用する方法
テストクラスに`@Testcontainers`を付与すると`@Container`が含まれるクラス内のコンテナ型フィールドを検索して、検索したフィールドが静的フィールドの場合、すべてのテストが実行される前に一度だけコンテナが起動されて、すべてのテストが完了したらコンテナを停止します。
これでテストごとにコンテナが起動されることはないですね。
```java
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
class CustomerServiceWithJUnit5ExtensionTest {

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

}
```

### シングルトンコンテナパターンを利用する方法
コンテナ定義を実装する共通クラスを用意して、それぞれのテストクラスで共通クラスを継承することで、共通クラスで定義したコンテナを継承したテストクラスで使いまわすという方法です。
こちらの方法は実際に試せていないですが、以下公式ドキュメントに実装サンプルがあるので今後試そうと思います。

https://testcontainers.com/guides/testcontainers-container-lifecycle/#_using_singleton_containers

# 感想
テストおよびローカル開発の環境構築が非常に楽になるOSSだと思い今後いろいろ使い方試して、この使い方なら開発やテスト楽になるという方法探していきたいと思います。
他にも気を付けるポイントはたくさんあると思うので、また気になるものがあれば記事にしたいと思います。

# 参考情報
https://testcontainers.com/
https://www.docker.com/ja-jp/blog/testcontainers-best-practices/
https://testcontainers.com/guides/testcontainers-container-lifecycle/#_using_junit_5_extension_annotations
https://testcontainers.com/guides/testcontainers-container-lifecycle/#_using_singleton_containers