Spring BootとTestcontainersで結合テストをする備忘録
この記事はNE Advent Calendar 2024の20日目の記事です。
はじめに
Spring Bootで結合テスト(インテグレーションテスト)をするにあたって、Testcontainersにより実際のDBを用いてテストを実行するためにやったことを記録しておきます。
背景
テストでも実際にDBにアクセスしたい
テストコードを書く場合、まずはテスト対象クラスの単体テストから始めますが、それだけでは不十分なケースもあります。例えばテスト対象がJpaRepositoryに依存していて、そのRepositoryに複雑な条件のクエリが記述されていた場合、Repositoryをモック(スタブ)にして開発者の意図した返り値を返すようにしてしまうと、テストの意味が薄れてしまいます。このようなデメリットを回避するため、こういったケースではテスト実行時にも実際にデータベースにアクセスしたいです。
テストでも「実際の」DBにアクセスしたい
私の参加するプロジェクトでは、本番のDBとしてMySQLを用いる一方でテスト時のDBとしてインメモリ型のH2 Databaseを用いており、結合テストをする場合は基本的にこれで事足りていました。しかしあるタイミングで、H2と互換性のないMySQL固有の関数を含むRepositoryに依存したテストを書く必要が出てきました。このままだとテストの実行ができないためRepositoryをモックにする必要があります。ただ今回、テスト対象の機能は「検索処理」であったため、それをしてしまっては結合テストの存在価値が大きく薄れてしまいます。これを回避するには、テスト実行時にも実際のMySQLにアクセスする必要があります。
Testcontainersの概要
Testcontainers is an open source library for providing throwaway, lightweight instances of databases, message brokers, web browsers, or just about anything that can run in a Docker container.
Testcontainers は、データベース、メッセージ ブローカー、Web ブラウザー、または Docker コンテナーで実行できるほぼすべてのものの使い捨ての軽量インスタンスを提供するオープン ソース ライブラリです。
「使い捨ての軽量インスタンス」という点が肝で、コンテナが立ち上がっているのはテストが実行されている間だけです。コンテナのライフサイクルを自分で管理する必要がないというのは大きなメリットかと思います。
今回使いたいのはデータベースのコンテナですが、WebブラウザのコンテナによるUIテストなんかも実行できるようです。
今回やりたいこと
- 使用したいDBはMySQL。
- スキーマ構造は単一のsqlファイルに定義されており、テスト実行前にそれを読み込みたい。
- マスタデータ(商品カテゴリなど)も単一のsqlに定義されていて、これもテスト時には実際のデータを使いたい。
- テスト時はSQLモードを緩和したい
- 結合テストの保守コストを低減するため、テストケースにて登録するデータはできるだけNotNull制約を回避したい。
- 例:カラムが
VARCHAR(64) NOT NULL
の場合、勝手に空文字が入ってほしい。
- 例:カラムが
- 結合テストの保守コストを低減するため、テストケースにて登録するデータはできるだけNotNull制約を回避したい。
設定
spring.datasource.url=jdbc:tc:mysql:8.4.0:///test?TC_INITSCRIPT=file:path/to/schema.sql&TC_MY_CNF=mysql_conf_override&jdbcCompliantTruncation=false&zeroDateTimeBehavior=convertToNull
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=test
spring.datasource.password=test
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=none
spring.sql.init.data-locations=file:path/to/master.sql
spring.sql.init.mode=always
[mysqld]
character_set_server=utf8mb4
collation-server=utf8mb4_general_ci
# テストデータの登録を楽にするため、STRICT_TRANS_TABLESを外す
sql-mode='ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'
- MySQLを使う
-
spring.datasource.url
にTestcontainersのJDBC URL(MySQL)を指定 -
spring.datasource.driver-class-name
: TestcontainersのJDBCドライバを指定
-
- スキーマ構造の読み込み
-
spring.datasource.url
にパラメータとしてTC_INITSCRIPT
にスキーマ構造の定義されたSQLファイルを指定し、Testcontainer起動時に実行されるようにする。 -
spring.jpa.hibernate.ddl-auto
にnone
を指定し、エンティティ定義からDDL(データ定義言語)の自動生成・実行をしないようにする。
-
- マスタデータの読み込み
-
spring.sql.init.data-locations
にマスタデータのSQLファイルパスを指定 -
spring.sql.init.mode
にalways
を指定し、指定のSQLでデータベースの初期化が行われるようにする。
-
- NotNull制約の回避
-
TC_MY_CNF
にテスト用のmy.cnf
が配置されたディレクトリを相対パスで指定。-
my.cnf
では、sql-mode
からSTRICT_TRANS_TABLESを外す。
-
-
jdbcCompliantTruncation
がtrue
だとmy.cnf
でSQLモードを緩和してもSTRICT_TRANS_TABLES
で接続してしまうためfalse
にセット。
-
テストを書く
@SpringBootTest // 1
@Transactional // 2
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 3
@SqlMergeMode(SqlMergeMode.MergeMode.MERGE) // 4
class SampleServiceTest {
@Test
@Sql({"SampleServiceTest/sampleMethod/sample_records.sql"})
void sampleMethod() {
// Arrange, Act, Assert
}
}
- 結合テストはSpring Bootのコンテキストで実行したいので指定
- 各テストケースをトランザクション配下で実行することで、テスト終了後にDBの状態をテスト前の状態にrollbackするために指定
- Springがデフォルト提供する組み込みデータベースでなくTestcontainersが用意したものを用いたいため指定
-
@Sql
アノテーションをクラスレベル(テスト用の初期ユーザーの用意など)とメソッドレベル(テストケースに必要なデータ)に指定した場合、両方のスクリプトがマージされて実行されるようにするため指定
これを各テストケースに毎回指定するのは面倒なので、自作のアノテーションにまとめて定義してしまうのが良いと思います。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@SpringBootTest
@Transactional
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@SqlMergeMode(SqlMergeMode.MergeMode.MERGE)
public @interface IntegrationTest {
}
おわりに
Testcontainersで快適なテストライフを。
NE株式会社のエンジニアを中心に更新していくPublicationです。 NEでは、「コマースに熱狂を。」をパーパスに掲げ、ECやその周辺領域の事業に取り組んでいます。 Homepage: ne-inc.jp/
Discussion