Spring Boot + Kotlin で Docker Compose 連携と Testcontainers を使ってみた

正月休みで時間があったので、Spring Boot の Docker 連携機能を使ってみました。
試してみたことは次の３つです。公式ドキュメントの内容をなぞったものになっていますが、Kotlin でやるときに迷った部分があるので同じような人の助けになればと思います。

1. アプリケーション起動時に MySQL コンテナを起動、接続先を自動設定
2. UT 起動時に MySQL コンテナを起動、接続先を自動設定
3. UT のコンテナ設定を利用してアプリケーション起動


## サンプルコード

https://github.com/ksugimori/demo-spring-boot-docker-compose

### 検証環境

|                             | version |
| :-------------------------- | :------ |
| Spring Boot                 | 3.4.1   |
| Kotlin                      | 1.9.25  |
| Docker                      | 27.4.0  |
| mybatis-spring-boot-starter | 3.0.4   |

## 1. アプリケーション起動時に MySQL コンテナを起動、接続先を自動設定

### spring-boot-docker-compose

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-docker-compose

Spring Boot 3.1.0 で導入された Docker Compose との連携機能。dependency に追加して compose.yaml を書くだけで、アプリケーション起動時にコンテナを自動的に起動し、DataSource にその接続先を設定してくれます。

### 使い方

#### build.gradle.kts で dependencies に spring-boot-docker-compose を追加。

```kotlin:build.gradle.kts
	developmentOnly("org.springframework.boot:spring-boot-docker-compose")
```

#### compose.yaml を書く

```yaml:compose.yaml
services:
  mysql:
    image: 'mysql:8.1.0'
    ports:
      - 3306
    environment:
      - 'MYSQL_ROOT_PASSWORD=passw0rd'
      - 'MYSQL_DATABASE=hoge'
    volumes:
      - './mysql/data:/var/lib/mysql'
      - './mysql/ddl:/docker-entrypoint-initdb.d'
```

:::note info
ホスト側のポート番号は指定不要です。コンテナ起動時にランダムなポートが割り当てられますが、Spring が自動的にそのポートに接続してくれます。便利！
:::

#### bootRun で起動

あとは通常通りに bootRun タスクでアプリケーションを起動すると、次のようなログが出て compose.yaml に記載したコンテナが起動します。

```bash
2025-01-11T17:25:35.263+09:00  INFO 70705 --- [demo-app] [           main] .s.b.d.c.l.DockerComposeLifecycleManager : Using Docker Compose file /Users/zzz/Documents/demo-app/compose.yaml
2025-01-11T17:25:35.593+09:00  INFO 70705 --- [demo-app] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container demo-app-1  Created
2025-01-11T17:25:35.594+09:00  INFO 70705 --- [demo-app] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container demo-app-1  Starting
2025-01-11T17:25:35.717+09:00  INFO 70705 --- [demo-app] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container demo-app-1  Started
2025-01-11T17:25:35.718+09:00  INFO 70705 --- [demo-app] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container demo-app-1  Waiting
2025-01-11T17:25:36.219+09:00  INFO 70705 --- [demo-app] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container demo-app-1  Healthy
```

## 2. UT 起動時に MySQL コンテナを起動、接続先を自動設定

### TestContainers について

https://testcontainers.com/

コンテナを利用したテストを行うためのライブラリです。テストコードからコンテナを起動させ、それを利用して本番環境に近い構成でテストを行えます。Spring Boot でも Testcontainers 自体は以前から利用できましたが、バージョン 3.1.0 から compose.yaml と同様に接続情報の自動設定が可能になったようです。

### dependency

```kotlin:build.gradle.kts
	testImplementation("org.springframework.boot:spring-boot-testcontainers")
	testImplementation("org.testcontainers:junit-jupiter")
	testImplementation("org.testcontainers:mysql") // Testcontainers の MySQL モジュール
```

### UT での使い方

static なフィールド（Kotlin の場合 companion object） に起動したいコンテナを記載し、`@Container`、`@ServiceConnection` アノテーションを付けるだけです。

:::note info
MyBatis を使っているので `@MyBatisTest` ですが `@SpringBootTest` でも同様です。
:::

```kotlin
@MybatisTest
class UserMapperTest {
    companion object {
        @Container
        @ServiceConnection
        val mySqlContainer = MySQLContainer("mysql:8.1.0")
    }

    @Test
    fun testHoge() {
      // 略
    }
}
```

Testcontainers には様々な Docker イメージに対応した「モジュール」が存在し、MySQL なら MySQLContainer、Oracle なら OracleContainer といったクラスが用意されています。モジュールが用意されていないイメージを利用する場合や、詳細なカスタマイズが必要な場合は汎用的なクラスとして GenericContainer が使えます。

モジュールは公式サイトで検索できます。

https://testcontainers.com/modules/?language=java

### 設定の共通化

static なフィールドで定義するため、そのままではコンテナ設定をテストクラスごとに書く必要があります。複数のテストクラスで共通化するためには object として外部に切り出し、

```kotlin
object MyContainers {
    @Container
    @ServiceConnection
    val mySqlContainer = MySQLContainer("mysql:8.1.0")
}
```

`@ImportTestcontainers` アノテーションでコンテナ定義をインポートします。

```kotlin
@MybatisTest
@ImportTestcontainers(MyContainers::class)
class UserMapperTest {
    @Test
    fun testHoge() {
      // 略
    }
}
```

### テーブル作成

`/docker-entrypoint-initdb.d` にマウントできれば良いのですが、MySQLContainer でのボリュームのマウント方法がよくわからなかったので次の方法で対処。

application.properties で `spring.sql.init.mode=always` を指定。コンテナ起動後、 `src/test/resources/schema.sql` が実行されるようになります。


## 3. UT のコンテナ設定を利用してアプリケーション起動

Configuration クラスを作成し、UTで利用している共通設定（MyContainers）をインポート

```kotlin
@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers::class)
class MyContainersConfiguration
```

`src/test` 配下に次のメイン関数を定義すると、bootTestRun タスクでアプリケーション起動が可能です。このときアプリケーション起動時には MyContainersConfiguration でインポートした MyContainers に記載したコンテナが起動します。

```kotlin
fun main(args: Array<String>) {
	fromApplication<DemoApplication>()
		.with(MyContainersConfiguration::class.java)
		.run(*args)
}
```

## 感想

昨年の JJUG CCC でこれらの機能を知りましたが利用経験が無かったので、とりあえず基本的なところを触ってみました。Docker Compose 連携については Docker の知識さえあればかなり簡単に使い始められますし導入のハードルは低そうです。

## 参考

[Development-time Services :: Spring Boot](https://docs.spring.io/spring-boot/reference/features/dev-services.html)
[ 開発時のサービス :: Spring Boot - リファレンスドキュメント](https://spring.pleiades.io/spring-boot/reference/features/dev-services.html)
[Testcontainers 公式のガイド](https://testcontainers.com/guides/testing-spring-boot-rest-api-using-testcontainers/#_add_schema_creation_script)