Testcontainersを利用してIntegrationTestを改善する

ogp

はじめに

こんにちは、マイグレーションチームの寺嶋です。

本記事では、ZOZOTOWNのマイクロサービスにおけるデータベースを参照したユニットテストの改善で得られた知見や工夫について紹介します。

背景と課題

ZOZOTOWNでは、数年前からリプレイスプロジェクトが実施されており、いくつものマイクロサービスが誕生しました。初期にJavaで作られたマイクロサービスのユニットテストが開発環境のデータベースを参照しており、テストで利用しているデータが更新・削除されてしまうとテストに失敗してしまうことが度々起きていました。また、接続しているデータベースがオンプレのSQL Serverを利用しており、CI上でユニットテストを実施できない状況でした。

そのため対象のユニットテストは次の問題を抱えていました。

  • ローカルPC上でしか実行できない
  • 実データを利用しているので今日通ったテストが明日落ちる(可能性がある)

このようなことから外部環境に依存しないユニットテストへ変更する必要がありました。

対象サービスの技術スタック

今回改善するマイクロサービスの技術スタックは次の通りです。

  • Java 11
  • Maven
  • Spring Boot
  • MyBatis
  • SQL Server
  • JUnit 4

ZOZOTOWNリプレイスプロジェクトでは全社技術スタックを統一しています。詳しくは下記の記事をご覧ください。

qiita.com

対応方法の検討

解決方法として次の方法を検討しました。

  • 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で必要となります。詳しくは下記のページをご覧ください。

www.testcontainers.org

接続先を動的に変更する

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になっており、詳細は次の通りです。

  1. 先頭文字は V から始める
  2. {VERSION}は実行される順番となり、小さい番号から実行される
  3. __はバージョンと説明との区切り
  4. {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では、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は次のリンクからぜひご応募ください。

hrmos.co

カテゴリー