Spring Bootシリーズ。今回はユニットテストです。
Springでは、テスト用のコンポーネントが用意されていて、Spring Bootではテストの実装も楽に実装できるようになっています。
今回はWebアプリケーションのコントローラをSpring Test + JUnit 5でユニットテストを実装します。
ユニットテスト(単体テスト)の単位
テスト対象とする単位をどうするか。テスト計画を立てるときに、この単位を決める必要があります。ユニットテストのユニットの意味は「単位」なので、定められた単位ごとにテストをする。これがユニットテストです。
メソッド単位
Javaであれば、一番小さい単位はメソッドですね。このメソッドをテスト単位とする、と解説しているサイトや参考書が多いように感じます。
publicメソッド単位
もう少し粗い単位として、publicメソッド単位とすることもあります。同一クラス内の共通処理をprivateメソッドで切り出した場合、そのメソッドは、呼び出し元のpublicメソッドの一部としてまとめてテストしてしまおう、というものです。
各層のコンポーネントのpublicメソッド単位
さらに粗い単位として、Web三層アプリケーションの各層のpublicメソッド単位とするものです。アプリケーション層のコントローラクラス、ドメイン層のサービスクラス、インフラストラクチャー層のリポジトリークラスのpublicメソッドをテスト対象の単位とします。
処理単位
そして一番粗い単位として、一連の処理を単位とするものです。Web三層アプリケーションであれば、コントローラクラスからサービスクラス、リポジトリークラスまでの呼び出しを一つの単位として、まとめてテストしてしまう方法です。
どのテスト単位を使うべきか
単位の粒度が粗くなるほど、テストの内容も粗くなります。
また、粗くすると、一つのテストケースに対するプログラムの範囲が広がるので、プログラムの修正で影響が及ぶテストケースが増えて、テストケースの修正範囲が広がります。
利点としては、単位が細かいほど、ユニットテストの実装工数が高くなります。
Web三層アプリケーションであれば、単位を「各層のコンポーネントのpublicメソッド単位」にするのが、テスト精度、実装工数のバランスが良いと感じます。
SIerの実際の現場では、工数優先で「処理単位」でテストをしている現場が多いのが残念ですが・・・
依存ライブラリの定義
では、テストを実行するために必要な依存ライブラリをbuild.gradleに定義します。
plugins { id 'java-library' id 'org.springframework.boot' version '2.4.5' } apply plugin: 'war' apply plugin: 'eclipse-wtp' apply plugin: 'io.spring.dependency-management' sourceCompatibility = '1.8' targetCompatibility = '1.8' compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' eclipse.wtp { component.contextPath = 'penguin-web' facet { facet name: 'jst.java', version: '1.8' facet name: 'wst.jsdt.web', version: '1.0' facet name: 'jst.web', version: '3.1' } } repositories { jcenter() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.h2database:h2' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'junit', module:'junit' exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } testImplementation 'org.mockito:mockito-inline:3.6.28' testImplementation 'net.java.dev.jna:jna-platform:5.8.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { useJUnitPlatform() testLogging { showStandardStreams = true events = ['started', 'skipped', 'passed', 'failed'] exceptionFormat = 'full' } }
今回追加したところを一つ一つ見ていきましょう。
1. テストライブラリの追加
依存関係に「spring-boot-starter-test」を追加することで、テストで必要なライブラリがまとめて追加されます。
testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'junit', module:'junit' exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' }
mavenリポジトリのサイトを確認すると、この依存関係でいくつかのライブラリが追加されることがわかります。
アサーションのライブラリである「AssertJ」や「Hamcrest」、モック化のライブラリである「mockito」、そして、JUnit本体である「JUnit Jupiter」が追加されることがわかります。
Spring Boot Starter Testのバージョン2.2から、JUnit 4の依存が削除され、代わって、JUnit 5 (Jupiter)の依存が追加されました。
しかし、「AssertJ」の依存関係に、JUnit 4の依存関係が含まれているため、JUnit 4のライブラリも追加されてしまいます。
JUnit 4を使わなければいいだけの話しなんですが、テストメソッドに使用する@TestアノテーションがJUnit 4とJUnit 5の両方にあって、誤ってimportすると事故の元なので、excludeでJUnit 4のライブラリを除外指定しています。
2. モックライブラリの追加
依存関係に「spring-boot-starter-test」を追加することで、モック化のライブラリである「mockito」が追加されます。
しかし、追加される「mockito-core」だけでは、finalメソッドやstaticメソッドのモック化はできません。「mockito-inline」ライブラリを依存関係に追加することで、finalメソッドやstaticメソッドのモック化が有効化されます。
testImplementation 'org.mockito:mockito-inline:3.6.28'
ただし、「mockito-inline」ライブラリを追加した状態で、テストクラスをJRE (Java Runtime Environment)で実行すると、次のような例外が発生します。
Caused by: org.mockito.exceptions.base.MockitoInitializationException: Could not initialize inline Byte Buddy mock maker. It appears as if you are running on a JRE. Either install a JDK or add JNA to the class path. Java : 16 JVM vendor name : AdoptOpenJDK JVM vendor version : 16+36 JVM name : OpenJDK 64-Bit Server VM JVM version : 16+36 JVM info : mixed mode, sharing OS name : Windows 10 OS version : 10.0 at org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker.<init>(InlineByteBuddyMockMaker.java:234) ~[mockito-core-3.6.28.jar:na]
このエラーを解消する方法は、例外メッセージにある通り、テストクラスをJREではなくJDK (Java Development Kit)で実行するか、JNA (Java Native Access)ライブラリをクラスパスに追加する必要があります。
実行方法に制約を持たせたくはないので、JNAを追加して、JREでもJDKでも実行できるようにします。
testImplementation 'net.java.dev.jna:jna-platform:5.8.0'
3. テストランチャーの追加
テストを起動するときに使用するテストランチャーを依存関係に追加します。
テストコードの実装時ではなく、テストの起動時のみ必要なライブラリなので、依存関係をtestRuntimeOnlyで指定しているところに注意が必要です。
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
4. gradle test実行時の設定
これまでの設定で、eclipseでJUnitを動かすことは可能ですが、gradle testコマンドでJUnitを動かすときは、以下の設定が必要になります。
test { useJUnitPlatform() testLogging { showStandardStreams = true events = ['started', 'skipped', 'passed', 'failed'] exceptionFormat = 'full' } }
もう少し、細かく見ていきましょう。
以下の設定は、gradle testでJUnitを使用することを指定しています。この設定がないと、JUnitのコードは実行できないので、必須の設定となります。
useJUnitPlatform()
以下の設定は必須ではありませんが、gradle testを実行するのであれば、入れておいたほうが良いでしょう。
この設定がないと、gradle testを実行してもコンソールはテストが成功したか失敗したかの情報しか表示されず、テスト結果の詳細は、build/reports/tests/testに出力されるテストレポートを確認することになります。
testLogging { showStandardStreams = true events = ['started', 'skipped', 'passed', 'failed'] exceptionFormat = 'full' }
showStandardStreams = true
テスト実行時の標準出力、標準エラー出力がコンソールに表示されるようになります。
events = ['started', 'skipped', 'passed', 'failed']
各テストメソッドの開始。スキップ、成功、失敗がコンソールに表示されるようになります。
結果だけが必要であれば、'passed'と'failed'だけを指定します。
exceptionFormat = 'full'
例外発生時にすべての情報が表示されるようになります。
build.gradleの変更を反映
build.gradleを編集したので、編集結果をeclipseに反映しましょう。
プロジェクトのトップ(プロジェクト名の部分)、もしくはbuild.gradleの上で右クリックして、表示されたメニューの[Gradle] - [Gradleプロジェクトのリフレッシュ]をクリックします。
次回予告
今回は、Spring Bootアプリケーションのユニットテストを実装・実行するために必要なライブラリの追加や、実行環境の整備をしました。
次回から、いよいよユニットテストを実装します。はじめはコントローラーのユニットテストを実装します。
では、次回の「Spring BootでテストするWebアプリケーション② - コントローラーのテスト」でお会いしましょう!!