Spring Boot 2.0.x の Web アプリを 2.1.x へバージョンアップする ( その10 )( @Rule を使用していないテストを JUnit 5 のテストに書き直す+JUnit 5 の Parallel 実行を試してみる )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
- JUnit 4 で書いているテストの内
@Rule
を使用しているテスト以外を JUnit 5 で書き直します。 - Spring Framework 5.0+JUnit 5 だと
@SpringBootTest
アノテーションが付与されたテストも parallel 実行できるようになったらしいので、試してみます。
- JUnit 4 で書いているテストの内
参照したサイト・書籍
Spring 5でSpring Testのここが変わる_公開版
https://www.slideshare.net/HasegawaDanna1/spring-5spring-testFINALLY JUNIT 5 HAS NATIVE SUPPORT IN GRADLE
https://alexanderontesting.com/2018/04/07/finally-junit-5-has-native-support-in-gradle/Junit 5 parallel test execution via Gradle fails with "Received a completed event for test with unknown id".
https://github.com/gradle/gradle/issues/6453劇的改善 Ci4時間から5分へ〜私がやった10のこと〜
https://www.slideshare.net/aha_oretama/ci-82258405Spring Framework Documentation - Testing
https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#integration-testingJUnit 5 User Guide - 2.18. Parallel Execution
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-executionAssertJ Core features highlight
http://joel-costigliola.github.io/assertj/assertj-core-features-highlight.html- 「Collect all errors with soft assertions」に JUnit 5 の assertAll と同じように1つの assert がエラーになっても全ての assert を実行する方法が記載されています。
目次
手順
JUnit 5 への変更方法
@RunWith(SpringRunner.class)
は JUnit 5 では不要なので削除します。- Spring Boot 2.1 から
@SpringBootTest
が付与されていると@ExtendWith(SpringExtension.class)
も付与されるようになったので、テストクラスには記述しません。@SpringBootTest
アノテーションのソース(org.springframework.boot.test.context.SpringBootTest)を見ると@ExtendWith(SpringExtension.class)
が記述されています。 @Test
アノテーションの import 文をimport org.junit.Test
→import org.junit.jupiter.api.Test;
に変更します。- テストメソッドから
public
を削除します。またthrows Exception
も例外が throw されない場合には削除します。 - Assertion Library は AssertJ のままにします(使い慣れていて機能も豊富で便利なので)。org.junit.jupiter.api.Assertions.* の Assertion Library には変更しません。
- 例外発生のテストも assertThrows には変更せず AssertJ の assertThatThrownBy 等のメソッドを使用したままにします(これについては JUnit 5 の assertThrows にした方がわかりやすいような気もしています)。
- AssertJ の assertThat を連続で記述している場合には JUnit 5 の assertAll を使用するよう変更します。
@Rule
を使用しているテストは JUnit 5 User Guide - 5. Extension Model で書き直す必要があるので、今回は変更しません。次回変更します。
テストの parallel 実行を試してみる
全体を変更する前に、Spring Framework 5+JUnit 5 から @SpringBootTest
アノテーションが付与されたテストクラスも parallel 実行できるようになったらしいので試してみます。
以下の仕様で試します。
- parallel 実行対象のテストクラスには
@Tag("parallel")
アノテーションを付与します。 - testJUnit5InParalell タスクを追加し、このタスクで
@Tag("parallel")
アノテーションが付与されたテストを parallel 実行します。 - test タスクは
@Tag("parallel")
アノテーションが付与されていないテストを順次実行します。
まずは build.gradle を変更します。
task testJUnit4AndSpock(type: Test) { jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=unittest"] testLogging { afterSuite printTestCount } } //task testJUnit5InParalell(type: Test, dependsOn: "testJUnit4AndSpock") { task testJUnit5InParalell(type: Test) { jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=unittest"] // for JUnit 5 useJUnitPlatform { includeTags "parallel" } systemProperties["junit.jupiter.execution.parallel.enabled"] = true systemProperties["junit.jupiter.execution.parallel.mode.default"] = "concurrent" maxParallelForks 2 testLogging { events "STARTED", "PASSED", "FAILED", "SKIPPED" afterSuite printTestCount } } test.dependsOn testJUnit5InParalell test { jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=unittest"] // for JUnit 5 useJUnitPlatform { excludeTags "parallel" } testLogging { events "STARTED", "PASSED", "FAILED", "SKIPPED" afterSuite printTestCount } }
task testJUnit5InParalell(type: Test) { ... }
を追加します。このタスクで@Tag("parallel")
アノテーションが付与されたテストを parallel 実行します。includeTags "parallel"
を記述して@Tag("parallel")
アノテーションが付与されたテストだけを実行対象にします。- 以下の3行を記述して parallel 実行を有効にします。
systemProperties["junit.jupiter.execution.parallel.enabled"] = true
systemProperties["junit.jupiter.execution.parallel.mode.default"] = "concurrent"
maxParallelForks 2
events "STARTED", "PASSED", "FAILED", "SKIPPED"
を記述してテストが parallel 実行されていることが確認できるようにします。
test.dependsOn testJUnit4AndSpock
→test.dependsOn testJUnit5InParalell
に変更します。testJUnit4AndSpock タスクは依存関係から外して JUnit 4 と Spock のテストが実行されないようにします。- test タスクの以下の点を変更します。
useJUnitPlatform()
→useJUnitPlatform { excludeTags "parallel" }
に変更して@Tag("parallel")
アノテーションが付与されたテストを実行対象外にします。testLogging { ... }
内にevents "STARTED", "PASSED", "FAILED", "SKIPPED"
を追加して、test タスクではテストが parallel 実行されていないことを確認できるようにします。
parallel 実行のテストクラスは LibraryHelperTest、Mail001HelperTest、Mail002HelperTest、Mail003HelperTest の4つを使用します。
src/test/java/ksbysample/webapp/lending/helper/library/LibraryHelperTest.java を JUnit 5 のテストクラスに変更します。このクラスには @Tag("parallel")
アノテーションは付与しません。
@SpringBootTest public class LibraryHelperTest { @Autowired private LibraryHelper libraryHelper; @MockBean private LibraryForsearchDao libraryForsearchDao; @Test void testGetSelectedLibrary_図書館が選択されていない場合() { given(libraryForsearchDao.selectSelectedLibrary()).willReturn(null); String result = libraryHelper.getSelectedLibrary(); assertThat(result).isNull(); } @Test void testGetSelectedLibrary_図書館が選択されている場合() { LibraryForsearch libraryForsearch = new LibraryForsearch(); libraryForsearch.setSystemid("System_Id"); libraryForsearch.setFormal("図書館名"); given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch); String result = libraryHelper.getSelectedLibrary(); assertThat(result).isEqualTo("選択中:図書館名"); } }
src/test/java/ksbysample/webapp/lending/helper/mail/Mail001HelperTest.java を JUnit 5 のテストクラスに変更します。このクラスには @Tag("parallel")
アノテーションを付与します。
@Tag("parallel") @SpringBootTest public class Mail001HelperTest { @Autowired private Mail001Helper mail001Helper; @Value("ksbysample/webapp/lending/helper/mail/assertdata/001/message.txt") ClassPathResource messageTxtResource; @Test void testCreateMessage() throws Exception { MimeMessage message = mail001Helper.createMessage("test@sample.com", 1L); assertAll( () -> assertThat(message.getRecipients(Message.RecipientType.TO)) .extracting(Object::toString) .containsOnly("test@sample.com"), () -> assertThat(message.getContent()) .isEqualTo(FileCopyUtils.copyToString(Files.newReader( messageTxtResource.getFile(), StandardCharsets.UTF_8))) ); } }
src/test/java/ksbysample/webapp/lending/helper/mail/Mail002HelperTest.java を JUnit 5 のテストクラスに変更します。このクラスには @Tag("parallel")
アノテーションを付与します。
@Tag("parallel") @SpringBootTest public class Mail002HelperTest { @Autowired private Mail002Helper mail002Helper; @Value("ksbysample/webapp/lending/helper/mail/assertdata/002/message.txt") ClassPathResource messageTxtResource; @Test void testCreateMessage() throws Exception { MimeMessage message = mail002Helper.createMessage(new String[]{"test@sample.com", "sample@test.co.jp"}, 1L); assertAll( () -> assertThat(message.getRecipients(Message.RecipientType.TO)) .extracting(Object::toString) .containsOnly("test@sample.com", "sample@test.co.jp"), () -> assertThat(message.getContent()) .isEqualTo(FileCopyUtils.copyToString(Files.newReader( messageTxtResource.getFile(), StandardCharsets.UTF_8))) ); } }
src/test/java/ksbysample/webapp/lending/helper/mail/Mail003HelperTest.java を JUnit 5 のテストクラスに変更します。このクラスには @Tag("parallel")
アノテーションは付与しません。
@SpringBootTest public class Mail003HelperTest { @Autowired private Mail003Helper mail003Helper; @Value("ksbysample/webapp/lending/helper/mail/assertdata/003/message.txt") ClassPathResource messageTxtResource; @Test void testCreateMessage() throws Exception { List<LendingBook> lendingBookList = new ArrayList<>(); // 1件目 LendingBook lendingBook = new LendingBook(); lendingBook.setBookName("x"); lendingBook.setApprovalResult(APPROVAL.getValue()); lendingBookList.add(lendingBook); // 2件目 lendingBook = new LendingBook(); lendingBook.setBookName(Strings.repeat("X", 128)); lendingBook.setApprovalResult(REJECT.getValue()); lendingBookList.add(lendingBook); MimeMessage message = mail003Helper.createMessage("test@sample.com", 1L, lendingBookList); assertAll( () -> assertThat(message.getRecipients(Message.RecipientType.TO)) .extracting(Object::toString) .containsOnly("test@sample.com"), () -> assertThat(message.getContent()) .isEqualTo(FileCopyUtils.copyToString(Files.newReader( messageTxtResource.getFile(), StandardCharsets.UTF_8))) ); } }
clean タスク実行 → Rebuild Project 実行 → build タスクを実行してみると、testJUnit5InParalell タスクでは parallel 実行されており(複数のテストが同時に STARTED になる)、test タスクでは順次実行されています(1つのテストが STARTED、PASSED になってから次のテストが STARTED になっています)。
ただし parallel 実行する testJUnit5InParalell タスクの開始、終了時に少し止まる時間があり(内部で何か準備と終了の処理をしていると思われる)、3回追加で実行してみたところ平均 2m 50s だったのですが、build.gradle の parallel 実行の設定をコメントアウトして3回実行してみたところ 2m 10s 前後で 30~40s 程 parallel 実行しない方が速い結果でした。parallel 実行した時としない時を比較して時間短縮のメリットが得られると判断した時だけ parallel 実行を選択した方がよいのかな。。。?(少なくとも今回のようにテストクラスが少ない場合には何のメリットもありません)。
task testJUnit5InParalell(type: Test) { jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=unittest"] // for JUnit 5 useJUnitPlatform { includeTags "parallel" } // systemProperties["junit.jupiter.execution.parallel.enabled"] = true // systemProperties["junit.jupiter.execution.parallel.mode.default"] = "concurrent" // maxParallelForks 2 testLogging { events "STARTED", "PASSED", "FAILED", "SKIPPED" afterSuite printTestCount } }
テストを書き直してから再度 parallel 実行を試してみたいと思いますので、parallel 実行可能なテストには @Tag("parallel")
アノテーションを付与します。
テストを書き直してから build タスクを実行する
今回は青字のテストを JUnit 5 に書き直しました。
clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると BUILD SUCCESSFUL が表示されました。問題ないようです。
parallel 実行ありの場合と parallel 実行なしの場合を比較してみると、40s ~ 1m くらい差が出ました。まだ parallel 実行の方が時間がかかるようです。
parallel実行あり | parallel実行なし | |
---|---|---|
1回目 | 6m 5s | 5m 3s |
2回目 | 6m 8s | 5m 13s |
3回目 | 5m 51s | 5m 12s |
build.gradle から parallel 実行用の testJUnit5InParalell タスクを削除した場合も試してみます。
task testJUnit4AndSpock(type: Test) { jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=unittest"] testLogging { afterSuite printTestCount } } test.dependsOn testJUnit4AndSpock test { jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=unittest"] // for JUnit 5 useJUnitPlatform() testLogging { afterSuite printTestCount } }
1回目 | 4m 51s |
2回目 | 4m 49s |
3回目 | 4m 41s |
更に速くなりました。テスト数が少ないのか parallel 実行に全然メリットが感じられません。。。 testJUnit5InParalell タスクは削除することにします。テストクラスに付与した @Tag("parallel")
アノテーションも削除します。
次回は。。。
テストの高速化を図るなら parallel 実行を使うよりは @SpringBootTest
を使用しないテストに書き直したり、テストの実行環境自体を分ける方が現実的なのかもしれません。
次回は @Rule
を使用しているテストを JUnit 5 の Extension で書き直します。
メモ書き
テストクラスを parallel 実行の対象にするが、クラス内のテストメソッドは parallel 実行しないようにするには?
build.gradle に testJUnit5InParalell タスクを記載している状態で、
task testJUnit5InParalell(type: Test) { jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=unittest"] // for JUnit 5 useJUnitPlatform { includeTags "parallel" } systemProperties["junit.jupiter.execution.parallel.enabled"] = true systemProperties["junit.jupiter.execution.parallel.mode.default"] = "concurrent" maxParallelForks 2 testLogging { events "STARTED", "PASSED", "FAILED", "SKIPPED" afterSuite printTestCount } }
src/test/java/ksbysample/webapp/lending/helper/library/LibraryHelperTest.java に @Tag("parallel")
を付与して parallel 実行すると、
@Tag("parallel") @SpringBootTest public class LibraryHelperTest { @Autowired private LibraryHelper libraryHelper; @MockBean private LibraryForsearchDao libraryForsearchDao; @Test void testGetSelectedLibrary_図書館が選択されていない場合() { given(libraryForsearchDao.selectSelectedLibrary()).willReturn(null); String result = libraryHelper.getSelectedLibrary(); assertThat(result).isNull(); } @Test void testGetSelectedLibrary_図書館が選択されている場合() { LibraryForsearch libraryForsearch = new LibraryForsearch(); libraryForsearch.setSystemid("System_Id"); libraryForsearch.setFormal("図書館名"); given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch); String result = libraryHelper.getSelectedLibrary(); assertThat(result).isEqualTo("選択中:図書館名"); } }
以下のキャプチャのようにテストが失敗することがありました。原因はこのテストクラスでは @MockBean
をフィールドに付与し各テストメソッドで given(...).willReturn(...);
で異なる挙動を定義しているため、testGetSelectedLibrary_図書館が選択されていない場合()
でモックを定義する(まだテストは未実施) → testGetSelectedLibrary_図書館が選択されている場合()
でモックを定義しテストも完了 → testGetSelectedLibrary_図書館が選択されていない場合()
でテストを実施するがモックの定義が変更されておりテスト失敗、となる場合があるためです。必ず失敗する訳ではなく成功する時もあります。
これを防止にするにはテストクラスに @Execution(ExecutionMode.SAME_THREAD)
アノテーションを付与して、テストクラスのテストが1つのスレッド内で実行されるようにします。
.......... import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; .......... @Execution(ExecutionMode.SAME_THREAD) @Tag("parallel") @SpringBootTest public class LibraryHelperTest {
build タスクを実行すると LibraryHelperTest クラス内のテストメソッドは順次実行されまることが確認できます。
履歴
2019/03/05
初版発行。