Spring BootのテストでTestcontainersを使って、データベースをコンテナとして起動してテストを実行してみる感じです。 これを使うことで、開発で使っているデータベースを汚染せずに簡単にデータベースまで通しのテストができそうな気がしています。
Testcontainers関連のライブラリを追加
build.gradle.kts
にTestcontainers
関連のライブラリを追加しています。
今回は、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