しおしお

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

Testcontainersのコンテナを複数テストクラスで使い回す

Testcontainersのコンテナを複数テストクラスで共有して使い回す方法になります。 コンテナを使い回すことで、テストクラスごとにコンテナが起動されテストの実行がめちゃ遅くなってしまう問題を解消することが期待できます。

コンテナをマニュアル起動するクラスを作成する

コンテナを手動起動するようなクラスを抽象クラスとして作ってあげます。

abstract class MySqlContainer {
    companion object {
        val mySqlContainer = MySQLContainer<Nothing>("mysql:5.7.32")
        init {
            mySqlContainer.start()
        }
    }
}

テストコード

コンテナを起動するクラスを親クラスに指定することで、コンテナが一度だけ起動するようになります。 Spring Bootのテストでは、@DynamicPropertySourceを使って、親クラスで起動したコンテナの情報をもとにデータベースの接続先情報などを上書きしてあげます。

@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@SpringBootTest
@AutoConfigureMockMvc
internal class UserHandlerTest(
        val mockMvc: MockMvc,
        val dataSource: DataSource
): MySqlContainer() {
    companion object {

        @DynamicPropertySource
        @JvmStatic
        fun changeProperty(registry: DynamicPropertyRegistry): Unit {
            val mySqlContainer = MySqlContainer.mySqlContainer
            registry.add("spring.datasource.url", mySqlContainer::getJdbcUrl)
            registry.add("spring.datasource.username", mySqlContainer::getUsername)
            registry.add("spring.datasource.password", mySqlContainer::getPassword)
        }
    }

        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")
    }
}

テストクラスの親クラスに指定できない場合…

すでに親クラスを指定済みで、そこに手を入れられないようなケースにはテストフレームワーク側の拡張機能を使って対応できます。 例えば、JUnit5の場合は以下のような拡張実装を作ることで対応できます。

class MySqlContainer: Extension {
    companion object {
        val mySqlContainer = MySQLContainer<Nothing>("mysql:5.7.32")
        init {
            mySqlContainer.start()
        }
    }
}

テストコード側では、@RegisterExtensionで拡張実装を登録し利用できます。

    companion object {
        @JvmField
        @RegisterExtension
        val mySqlContainer = MySqlContainer()

        @DynamicPropertySource
        @JvmStatic
        fun changeProperty(registry: DynamicPropertyRegistry): Unit {
            val mySqlContainer = MySqlContainer.mySqlContainer
            registry.add("spring.datasource.url", mySqlContainer::getJdbcUrl)
            registry.add("spring.datasource.username", mySqlContainer::getUsername)
            registry.add("spring.datasource.password", mySqlContainer::getPassword)
        }
    }

注意点

複数テストクラスでコンテナが共有されるので、クリーンな状態でテストが実行できなくなります。 Before系の処理などで、コンテナの中身をクリーンな状態にしてからテストを行う必要があります。