【Kotlin】自前の auto-configuration(Spring Boot)のユニットテストの書き方

プログラミング
この記事は約12分で読めます。

Spring Boot を利用してオレオレ開発していると、自己満 starter ライブラリを実装したくなりますね。そして、auto configure していい感じに DI したくなりますね?

そんなわけで、今回は自前ライブラリの auto-configuration のユニットテストの書き方を紹介します。

前提・環境など

実装例

先に実装例へのリンクを貼っておきます。このリポジトリを例に説明していきます。

norio-io/spring-auto-configuration-test-example-kotlin
Example of testing Spring Boot auto configuration for custom library in Kotlin - norio-io/spring-auto-configuration-test-example-kotlin

環境・バージョン情報

  • Gradle 6.2.2
  • Kotlin 1.3.70
  • Spring Boot 2.2.5.RELEASE
  • JUnit 5.5.2

自前の starter ライブラリと auto-configuration の作り方

普通にオフィシャルの説明が分かりやすかったです。

Spring Boot Reference Documentation
Spring Boot Reference Documentation

結論

素直に本家を真似て org.springframework.context.annotation.AnnotationConfigApplicationContext を使えばいいと思います。

auto-configuration に設定ファイル(application.ymlapplication.properties)が絡んでくる場合は、 org.springframework.boot.test.util.TestPropertyValues で設定値を指定します。

Spring Boot 1.4.0~2.0.0 未満は TestPropertyValues の代わりに下記を使用します。(API 見たら分かりますが似たような感じです)

org.springframework.boot.test.util.EnvironmentTestUtils

解説

プロジェクト構成

下記のように3つのサブプロジェクトから成るマルチプロジェクト構成としています。

  • foo:ライブラリ本体のプロジェクトです。Spring に依存しない純粋なプロジェクトです。
  • foo-spring-boot-autoconfigure:foo ライブラリを auto-configure するプロジェクトです。auto-configure するため、Spring Boot に依存します。プロジェクト名はオフィシャルの解説に従っています。
  • foo-spring-boot-starter:auto-configuration など foo ライブラリを利用するために必要なものを依存関係に含むプロジェクトです。ライブラリを使用する側は本プロジェクトのみを依存関係に追加すればいいようになっています。build.gradle のみでソースコードは持ちません。プロジェクト名はオフィシャルの解説に従っています。

foo-spring-boot-autoconfigurefoo-spring-boot-starter はひとつにまとめてしまうのもひとつの選択肢です。その場合は、サブプロジェクト名は foo-spring-boot-starter とする旨、オフィシャルでも解説されています。

個人的には分ける派です。

テスト対象のクラス

プロジェクト構成で脱線しかけましたが、今回は auto-configuration のユニットテストの解説なので、まずはテスト対象の auto-configure するクラス FooAutoConfigurationです。

この例では、auto-configurationの仕様は下記のようにします。

  • auto-configuration の対象は、ライブラリ本体が提供する Foo クラス。
  • 既に Foo クラスが Bean 登録されている場合は auto-configure しない。
  • プロパティファイル(application.yml または application.properties)で io.norio.foo.auto-configure が false に設定されている場合は auto-configure しない。(@Conditional系のアノテーションの仕様としては厳密ではないですが、本記事の本質ではないので気にしない)
  • auto-configure する場合、以下の通りプロパティファイルの設定値に従い Foo クラスの Bean を生成する。
  • io.norio.foo.string-prop の設定値を Foo クラスのプロパティ stringProp にセットする。設定値がない場合のデフォルト値は “default” とする。
  • io.norio.foo.strings-prop の設定値を Foo クラスのプロパティ stringsProp にセットする。設定値がない場合のデフォルト値は空配列とする。
  • io.norio.foo.int-prop の設定値を Foo クラスのプロパティ intProp にセットする。設定値がない場合のデフォルト値は -1 とする。
  • io.norio.foo.boolean-prop の設定値を Foo クラスのプロパティ booleanProp にセットする。設定値がない場合のデフォルト値は falseとする。

  • 10行目:Spring Boot なアプリケーションで Configuration クラスとして扱われるようにアノテーション付与しています。
  • 11行目:Foo クラスが既に Bean 登録されている場合は、この Configuration クラスを適用しない(auto-configureしない)ようにしています。
  • 12行目:この Configuration クラスを適用する条件を定義しています。要件次第ですが、この例では「io.norio.foo.auto-configure」プロパティに trueが設定されている場合、もしくは設定項目がない(matchIfMissing)場合に適用するようにしています。
  • 13行目:auto-configuration に使用する FooProperties クラスを DI するようにしています。(脱線するので詳細は割愛しますが、 FooProperties クラスによりプロパティファイルに定義した設定値が、auto-configuration にて使用される仕組みです。)
  • 16~22行目:auto-configurationの実装です。(Foo クラスを Bean 登録しています。)

それから、 Foo に関するプロパティを表現した FooProperties クラスです。auto-configuration にてデフォルト値を設定したり、application.yml の設定値を反映するために使用しています。

本家の auto-configuration においても application.yml の設定値を反映する auto-configuration においては大抵同様のクラス構成(AutoConfiguration クラス + Properties クラス)になっています。

  • 6行目:application.yml(application.properties)のio.norio.foo配下の設定項目を表現することを宣言しています。
  • 7行目:各プロパティは val で宣言され不変(イミュータブル)なため、setter で後から設定することができません。そのため、コンストラクタにてインスタンス生成時に全プロパティをバインドするように @ConstructorBinding アノテーションを付与しています。プロパティがミュータブルだと、思わぬところで勝手に設定変更された場合に、アプリケーションの挙動が不安定になりますからね。。なお、@ConstructorBinding はSpring Boot の 2.2.0 からの機能です。
  • 8行目:プロパティを保持するだけのクラスのためデータクラスとして宣言しています。
  • 9〜12行目:プロパティが未定義の場合のデフォルト値を定義しています。

テストクラス

各テストメソッドに必要なもの

FooAutoConfigurationに対するテストクラス FooAutoConfigurationTest について解説します。個々のテストメソッドの解説の前に、まずはフィールドなど冒頭部分の解説です。

  • 3行目:各テストメソッドで使用する ApplicationContext です。本家のお作法に従いましたが、auto-configuration をテストするので、 @Configuration を付与した JavaConfig をインプットとする AnnotationConfigApplicationContext を使用します。
  • 7行目:各テストメソッドの前に新しくコンテキストを作成します。
  • 12行目:各テストメソッドの後で使い終わったコンテキストを後片付けします。(この辺も正直深追いしていませんが、本家のお作法に従います。)

プロパティ定義なしのケースをテストする

では、ようやくテストメソッドを見ていきます。最初は、ライブラリを使う側でライブラリに関する設定値をプロパティファイルに一切定義しないことを想定したケースです。正に auto-configuration な感じ?

  • 8行目:auto-configuration のための FooAutoConfiguration クラスをコンテキストに登録しています。(コンポーネントスキャン的な?)
  • 9行目:お作法です。←  初期化みたいなもの?
  • 11行目:Foo クラスの Bean の取得を試みます。テストコード上、手動では Bean 登録していないので、これで取得できれば auto-configuration にて Bean 登録されたということです。
  • 13~16行目:仕様に従い、デフォルト値にて Foo の Bean が生成されたことを確認しています。

プロパティを一部定義したケースをテストする

プロパティのうち一部だけ定義したケースです。未定義のプロパティは FooProperties クラスに定義されたデフォルト値が使用されるので、プロパティ定義なしのケースとほとんど同じです。

  • 9、10行目:application.yml(application.properties)にプロパティが定義されているという状況を作り上げています。キーはプロパティ形式(ドット繋ぎ)で記載します。キーと値の区切りは半角イコールまたは半角コロンです。個人の好みでコロンにしていますが、キーがプロパティ形式なのでイコールにしておいた方が直感的かもしれません。なお、配列形式のプロパティは半角カンマ区切りで表現できます。yaml派としてはここも yaml 形式で書きたいところですが、yaml 形式もプロパティ形式もよしなに扱ってくれているのは Spring Boot 本体なので、ここは信じてプロパティ形式でテストが通れば OK とします。
  • 11行目:テストで使用するコンテキストにプロパティ値を適用しています。
  • あとは同様です。プロパティ定義した io.norio.foo.strings-prop は設定値が適用され、それ以外はデフォルト値であることを assert しています。

全プロパティを定義したケースをテストする

全プロパティを明示的に設定したケースです。特に追加の解説はありません。代表的なプロパティの型に対してサンプルを提示してみた感じです。

@ConditionalOnMissingBeanをテストする

先に解説したケースでは、手動で Foo クラスのBean登録をせずに auto-configuration された Bean のテストをしていました。次の例では、@ConditionalOnMissingBean について、Bean が存在する場合には auto-configuration されないことをテストします。

  • 15行目:手動で Foo クラスの Bean を登録しています。(アプリケーション側で自前で Bean 登録しているシーンを想定)
  • 16行目:Foo クラスの AutoConfiguration を登録しています。(spring.factories 的な?)
  • 19行目:Foo クラスの Bean を取得しています。(アプリケーションでいうところの Autowired するために Bean を取得している感じ)
  • 21〜24行目:手動で登録した Foo クラスのインスタンスであることを確認しています。

auto-configurationオフでBean登録ありのケースをテストする

auto-configuration が無効かつ手動で Bean 登録しているケースです。新しい説明事項はありませんが、他のケースとの対照のため掲載します。

auto-configurationオフでBean未登録のケースをテストする(異常系)

auto-configuration のように裏でよしなにやってくれる系のテストは、すんなり上手くいっても逆に心配になりますね?ということで、最後は角度を変えて、異常系で締め括りたいと思います。

  • 8行目:auto-configuration を無効にしています。
  • 9行目:FooAutoConfiguration を登録しています。しかし、手動での Foo クラスの Bean 登録は行いません。
  • 12行目:この状態で Foo クラスの Bean を取得しようとすると、想定通り NoSuchBeanDefinitionException が発生します。auto-configuration も手動での Bean 登録もしていないので当然の結果ですね。

その他のバリエーション

代表的なケースは先述の通りですが、その他のバリエーションもサンプルとして用意していますので、気になる方は下記 GitHub の FooAutoConfigurationTest クラスを参照ください。

norio-io/spring-auto-configuration-test-example-kotlin
Example of testing Spring Boot auto configuration for custom library in Kotlin - norio-io/spring-auto-configuration-test-example-kotlin

まとめ

あまり書くことが思い浮かばないです。ひとまず、Spring に関することで困ったら本家のGitHubを覗いてみるといいと思います。

そういえば、このブログを始めたころ、MacBook Pro が欲しいと嘆いていましたが、最近についに購入しました。16インチの late 2019 のやつです。やっぱり開発はWindowsよりMacが快適ですね。

コメント

タイトルとURLをコピーしました