💡

LocalStackとTestContainerを使ったクラウド再現UnitTest

2024/03/05に公開

はじめに

クラウド(AWS)にJavaアプリケーションをデプロイするような開発の場合、UnitTestでクラウドサービスをどう扱うかというのは、少なくとも自分は初期開発などでよく悩むポイントです。具体的にはAWSのサービスクライアントクラスをMock化してしまうのか、それともLocalStackなどのMockサービスを利用するのかなどです。
この記事では、LocalStackとTestContainerを用いたUnitTestの実装方法をメモがてら残します。

ソフトウェア・サービススタック

参考として、想定するソフトウェアやAWSサービスのスタックを羅列します。
特に変わった内容もなく、JavaとAWSを用いたシステムではありふれたものかと思っています。
記載は、粒度が大きものだけを抜粋。

  • Java Application
    • Spring Boot: 3.1.X
    • Spring Cloud
  • AWS
    • Aurora PostgreSQL
    • S3
    • SNS
    • SQS
    • ElastiCache for Redis
    • SSM Parameter Store
    • Cognito
    • ECS Fargate(Computeなのであまり関係はありませんが...)

やりたいこと

簡単に箇条書きになりますが記載します。

  • AWSサービスのクライアントをMock化するのではなく、Mockサービスでテストしたい
    • UnitTest実行時に自動的に起動して必要なリソースを自動で用意して利用したい
      • 例:SNSのTopic、S3のBucket、SQSのQueue
    • CIでも同様の構成で実行したい
  • DataBaseも本物を利用したい(今回は対象外)
    • 本物のRDBに対してクエリーを投げてUnitTestすることでエラーを早めに踏みたい
    • Flyway向けに管理されたSQLを起動時に一緒に流してしまいたい

課題

  • CognitoのLocal Mockサービスがない
    • LocalStackの有償版(Pro版)を利用すれば使えますが、都合により有償版(Pro版)は断念しました。
    • これはPro版を利用するというのが一番得策だと思うため、特に以降では触れません。
  • ElastiCache はLocalStackの有償版(Pro版)でなければ使えない(今回は対象外)
    • ElastiCache for Redis であれば、RedisをLocalのコンテナとして起動することで代替できるので、その方式としました。
  • RDS(Aurora)はLocalStackの有償版(Pro版)でなければ使えない(今回は対象外)
    • Aurora PostgreSQL であれば、 PostgreSQLをLocalのコンテナとして起動することで代替できるので、その方式としました。
  • TestContainer + LocalStack をあまり考えず利用すると毎回コンテナ再起動が走りとんでもない時間がかかってしまった。

決定

色々調べた結果、LocalStackだけでは成り立たないサービスもあったため、
TestContainerを用いて、各ミドルのContainerとLocalStackを一緒に管理することにします。

実装

実装の要所のみを説明します。
完全なサンプルコードは、そのうち実装してGitにて公開しようと思います。

ソースコードの構成(例)

登場人物を羅列したかっただけで、パッケージに意思はありません。
例です。

example.common                        // 適当なサンプルパッケージ
 |
 |-- AbstractUnitTestBase.java        // TestCaseの基底クラス<1>
 |
 |-- container
 |    |-- SingletonTestContainer.java // TestContainerとLocalStackの初期化
 |    `-- PostGisContainer.java
 |
 |-- model
 |    `-- AccessInfo.java             // LocalStackで起動したサービスへのアクセス情報
 |
 |-- helper
 |    |-- PostgresTestHelper.java       // PostgreSQLを初期化処理や利用するためのUtil
 |    |-- RedisTestHelper.java          // Redisを初期化処理や利用するためのUtil
 |    |-- S3TestHelper.java             // S3を初期化処理や利用するためのUtil
 |    |-- SnsTestHelper.java            // SNSを初期化処理や利用するためのUtil
 |    |-- SqsTestHelper.java            // SQSを初期化処理や利用するためのUtil
 |    `-- SsmTestHelper.java            // SSMを初期化処理や利用するためのUtil

AbstractUnitTestBase.java は、TestCaseクラスの親として利用します。
*TestHelper.java は、各サービスの初期化およびアクセス情報を管理するクラスです。
それらを束ねて処理するために、 SingletonTestContainer.java を用意します。

TestContainer の Singleton Containers

UnitTestのCase実行毎に、Containerの起動を行っていてはとてつもなく時間がかかりますので、
起動時に1回だけContainerを起動して初期化処理を行うようにするためには、 Singleton Containers を参考に実装する必要があります。
なお、特異なテストを行いたい場合は、別途自分自身でコンテナを立ち上げてテストするなども可能です。

SingletonTestContainerの説明

TestCaseの基底クラスから利用する処理にはなりますが、先に触れます。
TestCase実行時に、一度だけContainerの起動&初期化を行うようにしたクラスです。
簡単に真偽値で初期化制御を行っているだけです。

それぞれのサービス初期化クラスの例(S3TestHelper)

ほとんど同じようなことをしていますので、S3の例のみ取り扱います。
このクラスで処理することは、UnitTestに必要なS3バケットを作成することです。
S3Clientを使って、バケットを作成します。

汎用的にS3バケットを作成したい場合は、YAMLやJSONなどの外部ファイルに値を定義し、
このクラスの中で読み込み対応するなどすると良いです。

UnitTestの基底クラスの例(AbstractUnitTestBase)

とてもシンプルな基底クラスですが、以下が実装例です。
static initializer で singletonで管理されたコンテナの立ち上げおよび初期設定を行います。
その後、起動したコンテナの情報に合わせてSpringの設定値やシステムプロパティを設定します。
@DynamicPropertySource の使い方については 公式ドキュメント を参照するとよくわかります。
TestContainerとの組み合わせでよく使うものとして紹介されています。

全てのTestCaseはこのクラスを継承して作成することにより、
TestCase全体で1度だけコンテナ軌道が行われるようになります。

まとめ

TestContainerとLocalStackを使ってテスト時に一度だけコンテナ起動を行う構成のUnitTestの組み方を説明しました。
次回引き続き、PostgreSQL(PostGIS)のコンテナやRedisのコンテナを扱う方法についてメモ記事をUpする予定です。

Discussion