※本記事は、Mert Çalişkanによる”JUnit 5.6 Makes Testing Easy with New Features“を翻訳したものです。
テストの実行順序の定義、テストのパラレル実行などの新機能により、重要度の高いリリースが実現
著者:Mert Çalişkan
2020年5月4日
広く使われているJava単体テスト・フレームワークであるJUnitは、2017年に画期的なJUnit 5をリリースしました。10年間進化し続けてきたバージョン4.xを経て、JUnit 5ではフレームワーク全体が完全に書き直されました。節目となる5.0.0リリースの後、開発のペースを速めたJUnitチームは、4か月から5か月ごとにマイナー・リリースを行っています。最新のマイナー・バージョンとなる5.6.0は2020年1月20日に、その更新版となる5.6.1は3月22日にリリースされました。
本記事では、改めてJUnitフレームワークを取り上げ、導入された最新機能について、コード・サンプルを交えながら紹介します。さらに、関連機能についても、その機能がどのバージョンでフレームワークに導入されたのかがわかるように、「(5.x以降)」という形で補足しています。
まずは、用語の簡単な定義を記載しておきます。JUnit 5は、3つの独立したモジュールで構成されています。
- JUnit Platformは、JVMでテスト・フレームワークを実行する土台となります。多くのIDEやビルド・ツールがこのモジュールをサポートしています。
- JUnit Jupiterは、最新のプログラミング・モデルであり、JUnit 5テストのTestEngineでもあります。
- JUnit Vintageは、古いJUnit 3とJUnit 4のテスト用のTestEngineです。
(編集部より:ツールボックスにJUnit 4テストが含まれている方は、Brian McGlauflin氏による記事「JUnit 4からJUnit 5に移行する:重要な違いと利点」をお読みください。)
Mavenをお使いの方は、リスト1のようにして、JUnit 5.6の依存性を簡単に追加することができます。
リスト1:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
Gradleをお使いの方は、リスト2のようにして、依存性を追加することができます。
リスト2:
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.6.0'
それではいよいよ、新機能の紹介に入っていきます。
テストの実行順序の定義
あるクラスのテスト・メソッドの実行順序を、英数字順、ランダム、@Orderアノテーションによる指定順(5.4以降)、カスタムの順序定義のいずれかで定義できます。希望する順序の適用は、クラス・レベルに付加する@TestMethodOrderアノテーション(5.4以降)を使用すればうまくいきます。
テスト・メソッドをメソッド名の英数字順で実行する方法をリスト3に示します。
リスト3:
@TestMethodOrder(MethodOrderer.Alphanumeric.class)
class OrderedTest {
@Test
void testC() {
System.out.println("Test C");
}
@Test
void testZ() {
System.out.println("Test Z");
}
@Test
void testA() {
System.out.println("Test A");
}
}
リスト3での実行順序は、testA() -> testC() -> testZ()となります。
定義@TestMethodOrder(MethodOrderer.Random.class)を使用して、ランダムな順序を定義することもできます。この場合、メソッドの実行順序はランダムに選ばれます。テスト間に依存性がないという現在の状態を維持し、今後のステージでテスト間に余計な関係が構築されないようにしたい場合、この機能が意味を持つことになります。
テストの順番を明示的に定義したい場合は、@Orderアノテーションを使うことができます。リスト4をご覧ください。
リスト4:
class OrderedTest {
@Test
@Order(1)
void testZ() {
System.out.println("Test Z");
}
@Test
@Order(2)
void testC() {
System.out.println("Test C");
}
}
リスト4では、小さい値ほど優先されます。そのため、testZ()メソッドがtestC()メソッドより先に実行されます。@Orderアノテーションが付加されていないメソッドのデフォルト値は、Integer.MAX_VALUE / 2です(5.6以降)。
カスタムで順序を決めるメカニズムを定義し、@TestMethodOrderアノテーションでそのメカニズムを指定することもできます。リスト5の実装では、メソッド名の長さによってテスト・メソッドの実行順序が決まり、メソッド名が短いものの方が、長いものよりも先に実行されます。
リスト5:
class CustomOrder implements MethodOrderer {
@Override
public void orderMethods(MethodOrdererContext context) {
context.getMethodDescriptors().sort(
Comparator.comparingInt(
methodDescriptor -> methodDescriptor.getMethod()
.getName()
.length()));
}
}
CustomOrderの実装は、@TestMethodOrderアノテーションを使って指定する必要があります。リスト6をご覧ください。
リスト6:
@TestMethodOrder(CustomOrder.class)
class OrderedTest {
@Test
void a_very_long_test_method() {
}
@Test
void short_mthd() {
}
}
実行順序は、short_mthd() -> a_very_long_test_method()となります。
宣言的タイムアウトの定義
JUnitには、指定されたしきい値を超える時間がかかった場合にテストを失敗とマークするオプションがあります。この機能は、長時間実行されるテストを診断する場合にとても役立ちます。JUnit 5.5以降では、@Timeoutアノテーションを使ってタイムアウトの制限値を定義することができます。使用方法をリスト7に示します。
リスト7:
@Test
@Timeout(value = 1)
void checkJobDoesNotExceedLimit()
throws InterruptedException {
Thread.sleep(1500);
}
checkJobDoesNotExceedLimit()テスト・メソッドでのタイムアウト値を1秒に設定しています。時間の単位は、デフォルトでは秒です。このテストは1,500ミリ秒間実行されるため、失敗するのは明らかです。別の単位を指定する場合は、リスト8のようにします。
リスト8:
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
タイムアウト値をシステム・プロパティで定義することもできます。リスト9では、実行する各テストのタイムアウトの制限値を100ミリ秒と定義しています。
リスト9:
-Djunit.jupiter.execution.timeout.test.method.default=100ms
アノテーションでも値が設定されている場合、その値はシステム・プロパティで定義された値よりも優先されます。タイムアウト関連で指定できる、システム・プロパティのキーすべてと、その説明を以下に示します。
- junit.jupiter.execution.timeout.default:すべてのテスト可能なライフサイクル・メソッドに適用されるデフォルトのタイムアウト値
- junit.jupiter.execution.timeout.testable.method.default:すべてのテスト可能なメソッドに適用されるデフォルトのタイムアウト値
- junit.jupiter.execution.timeout.test.method.default:@Testアノテーションが付加されているメソッドに適用されるデフォルトのタイムアウト値
- junit.jupiter.execution.timeout.testtemplate.method.default:@TestTemplateアノテーションが付加されているメソッドに適用されるデフォルトのタイムアウト値
- junit.jupiter.execution.timeout.testfactory.method.default:@TestFactoryアノテーションが付加されているメソッドに適用されるデフォルトのタイムアウト値
- junit.jupiter.execution.timeout.lifecycle.method.default:すべてのライフサイクル・メソッドに適用されるデフォルトのタイムアウト値
- junit.jupiter.execution.timeout.beforeall.method.default:@BeforeAllアノテーションが付加されているメソッドに適用されるデフォルトのタイムアウト値
- junit.jupiter.execution.timeout.beforeeach.method.default:@BeforeEachアノテーションが付加されているメソッドに適用されるデフォルトのタイムアウト値
- junit.jupiter.execution.timeout.aftereach.method.default:@AfterEachアノテーションが付加されているメソッドに適用されるデフォルトのタイムアウト値
- junit.jupiter.execution.timeout.afterall.method.default:@AfterAllアノテーションが付加されているメソッドに適用されるデフォルトのタイムアウト値
@Timeoutアノテーションをクラス・レベルに付加して、そのクラスに含まれる各テスト・メソッドのデフォルトのタイムアウト値を指定することもできます。また、@Timeoutアノテーションは、@BeforeAll、@BeforeEach、@AfterEach、@AfterAllのいずれかのアノテーションが付加されているライフサイクル・メソッドでも使用できます。@TimeoutアノテーションはJUnit 5.6において試験運用版であるため、今後のリリースで変更される可能性もあります。
条件付きテスト実行
JUnit 5.1以降では、アノテーションで定義した条件に基づき、テストの実行をプログラム的に有効または無効にすることができます。このセクションでは、さまざまなカテゴリに属する、条件付きテスト実行に関連したアノテーションを示し、その使い方について説明します。
JRE条件:JREのバージョンに基づいて、テストの実行を有効または無効にすることができます。@EnabledOnJreアノテーション(5.1以降)により、指定したJREバージョンのみでテストの実行が有効になります。リスト10のテスト・メソッドは、Javaバージョン8のみで実行されます。
リスト10:
@Test
@EnabledOnJre(value = JRE.JAVA_8)
void shouldOnlyPassOnJava8() {
// ...
}
}
@DisabledOnJreアノテーション(5.1以降)は上記の逆であり、指定したJREバージョンでテストの実行が無効になります。JUnitバージョン5.6では、8から15までのJREバージョンを定義できます。
@EnabledForJreRangeおよび@DisabledForJreRange(いずれも5.6以降)という2つのアノテーションは、これらのアノテーションが付加されたメソッドの実行を特定のJREバージョン範囲で有効または無効にする条件を定義するためのものです。リスト11は、特定のJREバージョン範囲でテストの実行を有効にする例です。
リスト11:
@Test
@EnabledForJreRange(min = JRE.JAVA_9, max = JRE.JAVA_11)
void shouldRunBetweenJava8AndJava11() {
// ...
}
オペレーティング・システム条件:オペレーティング・システム(OS)の種類に基づいて、テストの実行を有効または無効にすることもできます。リスト12では、@EnabledOnOsアノテーション(5.1以降)により、macOSの場合にテストの実行が有効になります。
リスト12:
@Test
@EnabledOnOs(value = OS.MAC)
void shouldOnlyRunOnMacOs() {
// ...
}
@DisabledOnOSアノテーション(5.1以降)は上記の逆であり、指定した種類のOSでテストの実行が無効になります。OSの種類を表す値には、AIX、LINUX、MAC、SOLARIS、WINDOWS、OTHERを指定できます。
環境変数条件:環境変数の存在に基づいて、テストの実行を有効または無効にすることができます。@EnabledIfEnvironmentVariableアノテーション(5.1以降)により、指定した名前の環境変数が、指定した正規表現と一致する場合にテストの実行が有効になります。@DisabledIfEnvironmentVariableアノテーション(5.1以降)はその逆であり、テストの実行が無効になります。リスト13の例では、HOME環境変数が/Users/mcaliskanまたは/Users/mertcaliskanに設定されている場合にテストが実行されます。
リスト13:
@Test
@EnabledIfEnvironmentVariable(named = "HOME",
matches = "/Users/mcaliskan|/Users/mertcaliskan")
void shouldOnlyRunOnSpecifiedHOMEDirectory() {
// ...
}
システム・プロパティ条件:システム・プロパティの存在に基づいて、テストの実行を有効または無効にすることができます。@EnabledIfSystemPropertyアノテーション(5.1以降)により、指定した名前のシステム・プロパティが、指定した正規表現と一致する場合にテストの実行が有効になります。@DisabledIfSystemPropertyアノテーション(5.1以降)はその逆であり、テストの実行が無効になります。リスト14の例では、os.archシステム・プロパティの存在に基づいて、テストの実行が有効になります。このテストは、64ビット・バージョンのJREが使われている場合にのみ実行されます。
リスト14:
@Test
@EnabledIfSystemProperty(named = "os.arch",
matches = ".*64.*")
void worksOnlyOn64BitArchitecture() {
// ...
}
JUnit 5.6では、@EnabledIfEnvironmentVariable、@DisabledIfEnvironmentVariable、@EnabledIfSystemProperty、@DisabledIfSystemPropertyを繰り返すことができるようになりました。そのため、同じテスト・メソッドに複数回定義できるようになっています。JUnit 5.6では、@EnabledIfアノテーションと@DisabledIfアノテーションがコードベースから削除されています。この2つのアノテーションは、以前のリリースですでに非推奨になっていました。前述の各カテゴリで定義されているアノテーションを使うことをお勧めします。
プログラムによる拡張機能登録
拡張機能の主な目的は、テスト・クラスやテスト・メソッドの動作を拡張し、そのロジックを複数のテストで再利用できるようにすることです。JUnit 5では、拡張機能メカニズムの導入により、優れた手法が実現しました。Extension APIを使用して、テスト実行ライフサイクルの所定のポイントで一時停止し、フックされている拡張機能を実行することができます。
@RegisterExtensionアノテーション(5.1以降)により、プログラムから拡張機能を登録できます。リスト15に示すのは、テスト・メソッドの実行時間をロギングする拡張機能の例です。
リスト15:
public class LogTestExecutionTime
implements BeforeTestExecutionCallback,
AfterTestExecutionCallback {
private long startTime;
public void beforeTestExecution(ExtensionContext context)
{
startTime = System.currentTimeMillis();
}
public void afterTestExecution(ExtensionContext context) {
long elapsedTime = System.currentTimeMillis()
- startTime;
System.out.printf("Test Execution took %d ms", elapsedTime);
}
}
リスト16では、この拡張機能をテスト・クラス内に登録しています。
リスト16:
class ExtensionTest {
@RegisterExtension
LogTestExecutionTime logTestExecutionTime =
new LogTestExecutionTime();
@Test
void longRunningTest() {
// …
}
}
JUnit 5.5では、拡張機能を登録する際に制約が適用されます。その1つが、@RegisterExtensionアノテーションを付加するフィールドがprivateであってはならないというものです。もう1つが、拡張機能のクラスは、少なくとも1つのExtension APIを実装しなければならないというものです。バージョン5.4より前では、適切に構成されていない拡張機能は、何も表示されることなく無視されました。
テストのパラレル実行
デフォルトでは、JUnit Jupiterは1つのスレッドでテストを逐次実行します。しかし、バージョン5.3以降では、テストのパラレル実行も可能になっています。ただし、重要な注意事項があります。これは試験運用版の機能です。
まず、システム・プロパティで構成パラメータjunit.jupiter.execution.parallel.enabledをtrueに設定する必要があります。そして、テスト・メソッドまたはそれを含むクラスに、@Execution(ExecutionMode.CONCURRENT)アノテーション(5.3以降)を付加する必要があります。リスト17に示す簡単なテストの例では、エンドポイントを並列に5回テストしています。この実装では@RepeatedTestアノテーションを使っており、@RepeatedTestアノテーションには@TestTemplateアノテーションが使われています。
リスト17:
@Execution(ExecutionMode.CONCURRENT)
@RepeatedTest(value = 5, name = "{displayName} {currentRepetition}/{totalRepetitions}")
void testEndpointConcurrently() {
// …
}
パラレル実行構成のために定義された、システム・プロパティのキーは、junit.jupiter.execution.parallel.mode.default (A)とjunit.jupiter.execution.parallel.mode.classes.default (B)です。この2つにconcurrentまたはsame_threadのいずれかを適宜設定して、パラレル実行を有効にすることもできます。
図1は、この設定によって、1つのクラスに含まれるすべてのメソッドの実行と、トップレベルのクラスの実行がそれぞれどのようになるかを表しています。最初の列の(A, B)は、前述のキー(A)と(B)に、same_threadとconcurrentをどのように指定するかを表しています。

図1:テストのパラレル実行の例
テストのパラレル実行の詳細については、ドキュメントをご覧ください。
まとめ
JUnitチームは、4か月から5か月のリリース周期に合わせて、5.xのブランチに関する作業を懸命に進めています。再設計されたバージョンのJUnitで新機能がリリースされているのは歓迎すべきことです。
本記事では、リリース5.0から5.6までの間に導入された主要機能の一部を紹介しましたが、新機能は他にもあります。その中で特筆すべきは、生成されるJUnitアーティファクトにOSGiメタデータが含まれるようになったことです。そのため、お気に入りのOSGiコンテナでも簡単にJUnitを使えるようになっています。同様に注目すべきは、バージョン5.6以降のJUnitでは、Gradleモジュールのメタデータも公開されていることです。このメタデータは、Gradleユーザー向けの、バリアントに対応したきめ細かい依存性解決メカニズムです。
ただし、一部のJUnit 5 APIはまだ変更される可能性がある点に注意してください。JUnitチームは、パブリックな型に@APIアノテーションを付加し、Experimental、Maintained、Stableなどの値を割り当てています。
Learn More
![]() |
Mert ÇalışkanMert Çalışkan(@mertcal):Java Champion。『PrimeFaces Cookbook』(Packt Publishing、2013年)および『Beginning Spring』(Wiley Publications、2015年)の共著者。最新刊となる『Java EE 8 Microservices』を出版したばかりであり、AtlassianでBitbucketのプリンシパル・エンジニアを務めている。 |

