しおしお

IntelliJ IDEAのことなんかを書いてます

Spring BootのテストでTestcontainersを使ってみる

Spring BootのテストでTestcontainersを使って、データベースをコンテナとして起動してテストを実行してみる感じです。 これを使うことで、開発で使っているデータベースを汚染せずに簡単にデータベースまで通しのテストができそうな気がしています。

Testcontainers関連のライブラリを追加

build.gradle.ktsTestcontainers関連のライブラリを追加しています。 今回は、MySqlで試したので、org.testcontainers:mysqlを入れています。

testImplementation(platform("org.testcontainers:testcontainers-bom:1.14.1"))
testImplementation("org.testcontainers:mysql")
testImplementation("org.testcontainers:junit-jupiter")

テスト対象

エンドポイント

テスト対象は、データベースに変更を加える登録処理とテーブルの全件を返すような一覧取得用のエンドポイントとしています。

private fun myRouter(
    userHandler: UserHandler
) = router {
    "/api".nest {
        POST("/users", userHandler::addUser)
        GET("/users", userHandler::findAll)
    }
}

テーブル定義

名前だけ持つ簡単なテーブルにしています。

create table user (
    id int not null auto_increment,
    name varchar(200) not null,
    primary key (id)
);

テストの準備

テストコード

  • @Testcontainersアノテーションをテストクラスに追加します
  • @Containerアノテーションをつけて、テストで使用するコンテナを指定します
    今回は、MySqlを使うのでMySQLContainerとなります
  • @DynamicPropertySourceをつけたメソッドで、コンテナで起動したデータベースのURLなどを設定値として登録します
  • BeforeEachでテスト対象のデータベースの内容をFlywayで初期化して登録のケースなどのデータベース変更の影響を受けないようにしています
    テストで使うデータは、test配下のdb/migrationにRepeatableなやつとしておいておいてバージョンなどの影響を受けずに必ず最後に実行するようにしています

アノテーションつけるだけでさくっとコンテナでテストできるのめっちゃ便利ですね。 考えないといけないのは、テストで使うデータのセットアップなどですね。

@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
internal class UserHandlerTest(
        val mockMvc: MockMvc,
        val dataSource: DataSource,
        val flyway: Flyway
) {

    @BeforeEach
    internal fun setUp() {
        flyway.clean()
        flyway.migrate()
    }

    companion object {

        @JvmStatic
        @Container
        val container = MySQLContainer<Nothing>()

        @DynamicPropertySource
        @JvmStatic
        fun changeProperty(registry: DynamicPropertyRegistry): Unit {
            println("container.jdbcUrl = ${container.jdbcUrl}")
            registry.add("spring.datasource.url", container::getJdbcUrl)
            registry.add("spring.datasource.username", container::getUsername)
            registry.add("spring.datasource.password", container::getPassword)
        }
    }

    @Test
    internal fun ユーザが登録出来るよ() {
        val table = Table(dataSource, "user")
        val changes = Changes(table)
        changes.setStartPointNow()

        mockMvc.post("/api/users") {
            contentType = MediaType.APPLICATION_JSON
            content = """{"name": "siosio"}"""
        }.andExpect {
            status { isOk }
        }

        changes.setEndPointNow()
        Assertions.assertThat(changes)
                .hasNumberOfChanges(1)
                .changeOfCreation()
                .rowAtEndPoint().value("name").isEqualTo("siosio")
    }

    @Test
    internal fun ユーザ一覧が取得できるよ() {

        mockMvc.get("/api/users") {
        }.andExpect {
            status { isOk }
            content { contentType(MediaType.APPLICATION_JSON) }
            jsonPath("$.users.length()") { value(3) }
            jsonPath("$.users[*].name") { value(contains("user_1", "user_2", "user_3")) }
            jsonPath("$.users[*].id") { value(contains(1, 2, 3)) }
        }
    }
}

テスト用のデータベース用のデータ

Repeatableなマイグレーションファイルとしてtest/resources/db/migration/R__replace_user_table.sqlを作って、テスト対象テーブルのデータをセットアップしています。

truncate table user;
insert into user (name) values ('user_1');
insert into user (name) values ('user_2');
insert into user (name) values ('user_3');

テスト実行

こんな感じにログがでて、コンテナが起動されてテストが実行されます。 わりとさくっと、コンテナ使ってテスト実行が出来て便利な感じがあります。

07:10:33.048 [Test worker] DEBUG org.testcontainers.images.AbstractImagePullPolicy - Using locally available and not pulling image: mysql:5.7.22
07:10:33.048 [Test worker] DEBUG 🐳 [mysql:5.7.22] - Starting container: mysql:5.7.22
07:10:33.048 [Test worker] DEBUG 🐳 [mysql:5.7.22] - Trying to start container: mysql:5.7.22
07:10:33.049 [Test worker] DEBUG 🐳 [mysql:5.7.22] - Trying to start container: mysql:5.7.22 (attempt 1/3)
07:10:33.049 [Test worker] DEBUG 🐳 [mysql:5.7.22] - Starting container: mysql:5.7.22
07:10:33.049 [Test worker] INFO 🐳 [mysql:5.7.22] - Creating container for image: mysql:5.7.22

サンプルコード

サンプルプロジェクトは↓ github.com