はじめに
こんにちは、マイグレーションチームの寺嶋です。
本記事では、ZOZOTOWNのマイクロサービスにおけるデータベースを参照したユニットテストの改善で得られた知見や工夫について紹介します。
背景と課題
ZOZOTOWNでは、数年前からリプレイスプロジェクトが実施されており、いくつものマイクロサービスが誕生しました。初期にJavaで作られたマイクロサービスのユニットテストが開発環境のデータベースを参照しており、テストで利用しているデータが更新・削除されてしまうとテストに失敗してしまうことが度々起きていました。また、接続しているデータベースがオンプレのSQL Serverを利用しており、CI上でユニットテストを実施できない状況でした。
そのため対象のユニットテストは次の問題を抱えていました。
- ローカルPC上でしか実行できない
- 実データを利用しているので今日通ったテストが明日落ちる(可能性がある)
このようなことから外部環境に依存しないユニットテストへ変更する必要がありました。
対象サービスの技術スタック
今回改善するマイクロサービスの技術スタックは次の通りです。
- Java 11
- Maven
- Spring Boot
- MyBatis
- SQL Server
- JUnit 4
ZOZOTOWNリプレイスプロジェクトでは全社技術スタックを統一しています。詳しくは下記の記事をご覧ください。
対応方法の検討
解決方法として次の方法を検討しました。
- H2データベースを利用する
- Dockerコンテナのデータベースに接続する
H2データベースはJVM上にて動作するデータベースでインストールを必要としません。JDBCのURLに;MODE=MySQL
といったオプションをつけることでH2データベースの挙動をMySQL、PostgreSQLなど切り替えることができます。もちろんSQL Serverモードもあり、;MODE=MSSQLServer
を指定すればSQL Server風の挙動を再現させることが可能になります。ただ、H2のドキュメントを確認していると、ヒント句は破棄される
という説明があり、SQLチューニングでヒント句を使用しているサービスなので、採用は見送ることになりました。
ということで、DockerコンテナでSQL Serverを起動させてテストする方法となり、見つけたのがTestcontainers
でした。
Testcontainersとは
TestcontainersはJUnitのテストをサポートするJavaライブラリです。一般的なデータベースやSelenuim、Dockerコンテナで実行できるものを軽量で使い捨て可能なインスタンスとして提供してくれます。Testcontainersを利用すると次の種類のテストが簡単に行えます。
データアクセスレイヤーテスト
MySQL、PostgreSQL、Oracle Databaseなどのコンテナ化されたインスタンスを使用して、データアクセスレイヤーにコードの変更なくテストを実行できます。Dockerコンテナを利用するので複雑なセットアップも必要ありません。
アプリケーション統合テスト
データベース、メッセージキュー、Webサーバなどの依存関係を使用してアプリケーションのテストを実行できます。
UI受け入れテスト
自動化されたUIテストを実施するためにSelenuimと互換性のあるコンテナを使用し、ブラウザの状態やバージョンを気にすることなくテストを実施できます。また、失敗したテストのみ動画を録画するなども行ってくれます。
今回はデータアクセスレイヤーテスト
を活用して、実データベースを参照しているテストを改善します。
開発環境のデータベースから切り離す
pom.xml
Testcontainersの依存関係をpom.xmlに追記していきます。今回はSQL Serverコンテナをユニットテスト時に起動しますのでorg.testcontainers.mssqlserver
を追加しています。MySQLやPostgreSQL、Oracle Databaseを利用する場合は対象データベースのdependencyがありますので、環境に応じて指定してください。
<dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.16.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mssqlserver</artifactId> <version>1.16.3</version> <scope>test</scope> </dependency>
SQL Serverコンテナの起動
MSSQLServerContainer
クラスがSQL Serverコンテナを管理するクラスとなっており、これを継承し新たにMyMSSQLContainer
クラスを作っていきます。
public class MyMSSQLContainer extends MSSQLServerContainer<MyMSSQLContainer> { private static final String IMAGE_VERSION = "mcr.microsoft.com/azure-sql-edge:1.0.5"; private static MyMSSQLContainer container; private MyMSSQLContainer() { // (1) super(DockerImageName.parse(IMAGE_VERSION) .asCompatibleSubstituteFor("mcr.microsoft.com/mssql/server")); } public static MyMSSQLContainer getInstance() { if (container == null) { // (2) container = new MyMSSQLContainer() .waitingFor(Wait.forLogMessage("*SQL Server is now ready for client connections*", 1)) .acceptLicense(); } return container; } @Override public void start() { super.start(); } @Override public void stop() {} }
SQL Serverのコンテナは通常mcr.microsoft.com/mssql/server
イメージを利用すればよいのですが、M1 Macでは起動できません。M1 Mac上でも起動できるSQL Serverコンテナはmcr.microsoft.com/azure-sql-edge
イメージになります。(1)でazure-sql-edge
を指定し、mcr.microsoft.com/mssql/server
として振る舞うように設定しています。M1 Macをご利用の方はご注意ください。
本クラスはシングルトンでインスタンスを管理しています。複数のユニットテストでMyMSSQLContainer
クラスのインスタンスを作成してしまうと、それぞれのテストでSQL Serverコンテナを起動してしまうため、実行単位で起動を促しています。また、(2)でインスタンスを作成する際にメソッドチェインで呼び出しているacceptLicense
メソッドはライセンス認証をしています。SQL ServerやIBM Db2で必要となります。詳しくは下記のページをご覧ください。
接続先を動的に変更する
MyMSSQLContainer
クラスでSQL Serverコンテナの起動準備ができました。ただ、見てもらえればわかるようにデータベースのIDやパスワード、ポートを指定していません。ID・パスワードはTestcontainers
がデフォルトで設定しているものを利用し、ポートも指定しなければランダムで設定されます。パスワードはwithPassword
メソッド、ポートはwithExposedPorts
メソッドで設定できますが、ユニットテストなので固定化せず、デフォルト指定されるものを利用します。
public class AbstractDBTest { protected static MSSQLServerContainer<MyMSSQLContainer> sqlserver = MyMSSQLContainer.getInstance(); static { sqlserver.start(); } @DynamicPropertySource static void setup(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", sqlserver::getJdbcUrl); registry.add("spring.datasource.username", sqlserver::getUsername); registry.add("spring.datasource.password", sqlserver::getPassword); } }
AbstractDBTest
クラスはユニットテストクラスが継承する基底クラスとなります。Spring Bootではこちらにもある通り@DynamicPropertySource
を用いると容易に接続先を切り替えることができます。起動しているSQL Serverコンテナから接続に必要な情報を取得し環境変数に設定します。
テーブルとデータの復元
ここまでで、ユニットテスト起動時に必要な次の準備が完了しました。
- SQL Serverコンテナの起動
- 接続先の動的な切り替え
次はSQLの動作確認に必要なテーブルとデータの復元になります。
テーブル、データの復元にはFlywayを使っていきます。Flywayはデータベースのバージョン管理ツールで、DDLやDMLのSQLファイルをバージョン管理することで常に最新状態を保つことができます。pom.xmlにFlywayの依存関係を追加していきます。
<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> <version>8.5.8</version> <scope>test</scope> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-sqlserver</artifactId> <version>8.5.8</version> <scope>test</scope> </dependency>
テストで利用するDDLとDMLはtestディレクトリ配下のresources/db/migration
に次のファイル名で配置します。
- V1__CreateTable.sql
- V2__InitData.sql
SQLファイルのネーミングルールはV{VERSION}_{DESCRIPTION}.sql
になっており、詳細は次の通りです。
- 先頭文字は
V
から始める {VERSION}
は実行される順番となり、小さい番号から実行される__
はバージョンと説明との区切り{DESCRIPTION}
はバージョンの説明を記述する
Spring Boot起動時にFlyway.migrate()
が呼び出されるようにするため、FlywayMigrationStrategy
インタフェースの実装をBeanに登録します。
@Bean public FlywayMigrationStrategy cleanMigrateStrategy() { FlywayMigrationStrategy strategy = flyway -> { flyway.clean(); flyway.migrate(); }; return strategy; }
あとは、既存のテストクラスでAbstractDBTest
クラスを継承するとユニットテスト実行時にSQL Serverコンテナが起動し、テーブル・データの復元を行いテストを実行してくれます。テスト終了後にはSQL Serverコンテナは自動で終了してくれます。
まとめ
Testcontainers
を使ったユニットテストの改善・導入をご紹介しました。本対策をすることでCI上でもユニットテストの実行ができるようになり、機能追加や改修、リファクタリング時のリグレッションテストとして機能するようになりました。Dockerコンテナを利用することで速度の懸念もありましたが、SQL Serverの起動はそれほど遅くなく、テスト時間が伸びて待ちが発生するようなこともありませんでした。実データベースの参照がなくなりデータの状態に左右されず安定してユニットテストを実行できるようになったので、安心感という大きな恩恵を得ることができたと思います。
おわりに
ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は次のリンクからぜひご応募ください。