1. 概要
このドキュメントは、実際にテストを書くプログラマーや拡張機能の作者、テストエンジンの作者、それからビルドツールや IDE ベンダーのための包括的なリファレンスです。
1.1. JUnit 5 とは何か
過去の JUnit と違って、JUnit 5 は3種類のサブプロジェクトが提供するさまざまなモジュールを組み合わせたソフトウェアになっています。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform は JVM で テストフレームワークを起動する ための基盤を提供します。
Platform で実行するテストフレームワークを開発するための TestEngine
API も公開しています。
さらに、コマンドラインから Platform を起動するための Console Launcher や、JUnit 4 の環境で任意の TestEngine
を実行するための JUnit 4 based Runner を提供します。
JUnit Platform を最上級の機能として対応している IDE もたくさんあります(IntelliJ IDEA, Eclipse,
NetBeans, Visual Studio Code)。
ビルドツールについても同様です(Gradle, Maven, Ant)。
JUnit Jupiter は JUnit 5 でテストを書いたり、JUnit 5 の拡張機能を作成するための新しい プログラミングモデル や 拡張モデル を組み合わせたものです。
Jupiter サブプロジェクトは、Platform で Jupiter のテストを実行するための TestEngine
を提供します。
JUnit Vintage は Platform で JUnit 3 や JUnit 4 に基づくテストを実行するための TestEngine
を提供します。
クラスパスあるいはモジュールパスに JUnit 4.12 より新しいバージョンの jar ファイルを配置しなければなりません。
1.2. 対応している Java のバージョン
JUnit 5 を実行するには Java 8 より新しいバージョンが必要です。 しかし、Java 8 より古いバージョンの JDK でコンパイルしたテストコードも実行できます。
1.3. 分からないことの調べ方
JUnit 5 に関する質問は Stack Overflow や JUnit チームの Gitter へ投稿するといいでしょう。
1.4. JUnit 5 の始め方
1.4.1. JUnit のアーティファクトをダウンロードする
あなたのプロジェクトに追加するべきアーティファクトを調べるには、 依存ライブラリに関するメタデータ を参照してください。 依存ライブラリとして追加するには ビルドツールでテストを実行する や プロジェクト例 を参照してください。
1.4.3. プロジェクト例
Junit 5 のサンプルプロジェクトリポジトリ
には、そのまま実行できるサンプルプロジェクトがあるので、自分の環境にコピーして実験してみてください。
JUnit Jupiter を使用するプロジェクトだけでなく、JUnit Vintage や他のテストフレームワークを使用するプロジェクトもあります。
ビルドスクリプトも参考になるでしょう(build.gradle
や pom.xml
など)。
ビルドツールやプログラミング言語の組み合わせが特徴的なプロジェクトのリンクは次のとおりです。
-
Gradle と Java -
junit5-jupiter-starter-gradle
-
Gradle と Kotlin -
junit5-jupiter-starter-gradle-kotlin
-
Gradle と Groovy -
junit5-jupiter-starter-gradle-groovy
-
Maven -
junit5-jupiter-starter-maven
2. テストを作成する
次のコード例は、JUnit Jupiter で作成した最小限のテストです。 この章では全ての機能を説明していきます。
import static org.junit.jupiter.api.Assertions.assertEquals;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class MyFirstJUnitJupiterTests {
private final Calculator calculator = new Calculator();
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
2.1. アノテーション
JUnit Jupiter はテストを書いたりフレームワークを拡張するためのアノテーションを提供しています。
明記しない限り、全ての主要なアノテーションは junit-jupiter-api
モジュールの org.junit.jupiter.api
パッケージにあります。
アノテーション | 内容 |
---|---|
|
そのメソッドがテストメソッドであることを示します。JUnit 4 の |
|
そのメソッドが パラメタライズドテスト であることを示します。オーバーライド しなかったメソッドは 継承 します。 |
|
そのメソッドが 繰り返し実行するテスト のテンプレートであることを示します。オーバーライド しなかったメソッドは 継承 します。 |
|
そのメソッドが 動的に生成するテスト のファクトリメソッドであることを示します。オーバーライド しなかったメソッドは 継承 します。 |
|
そのメソッドが テストケースのテンプレート であることを示します。登録した テンプレートプロバイダー の返す呼び出しコンテキストの数に応じて、複数回実行するために設計します。オーバーライド しなかったメソッドは 継承 します。 |
|
|
|
このアノテーションで修飾したクラスにおける テストメソッドの実行順序 を変更します。JUnit 4 の |
|
このアノテーションで修飾したクラスにおけるインスタンスのライフサイクル を変更します。アノテーションは 継承 しません。 |
|
テストクラスやテストメソッドに独自の 表示名 を設定します。アノテーションは 継承 しません。 |
|
テストクラスに独自の 表示名生成ロジック を設定します。アノテーションは 継承 します。 |
|
当該テストクラスについて、 |
|
当該テストクラスについて、 |
|
当該テストクラスについて、 |
|
当該テストクラスについて、 |
|
その内部クラスが ネストテストクラス であることを示します。「クラス単位(per-class)」で インスタンスのライフサイクル を管理していないなら、ネストテストクラスには |
|
クラスやメソッドのいずれかに、 テストを選択するためのタグ を定義します。TestNG におけるテストグループ、JUnit 4 におけるカテゴリーに相当します。このアノテーションでクラスを修飾した場合は 継承 します。メソッドの場合は 継承 しません。 |
|
そのクラスやメソッドが 無効 であることを示します。JUnit 4 では |
|
|
|
クラスを修飾して、 登録する拡張機能を宣言的に記述 します。アノテーションは 継承 します。 |
|
フィールドを修飾して、 登録する拡張機能を手続き的に記述 します。フィールドは 隠蔽 されない限り 継承 します。 |
|
フィールドや、テストメソッドおよびライフサイクルメソッドの引数へ、 一時ディレクトリ の情報を注入します。パッケージは |
一部のアノテーションは 実験的な機能 です。詳しい状態は 実験的な API を参照してください。 |
2.1.1. メタアノテーションと合成アノテーション
JUnit Jupiter のアノテーションは メタアノテーション として使用できます。 つまり、自分で定義した 合成アノテーション は、使用したメタアノテーションのセマンティクスを自動的に 継承 できるのです。
例えば、コードベースのあちこちに @Tag("fast")
というコード片をコピーペーストで量産する代わりに、@Fast
という 合成アノテーション を定義できるのです。
そうすれば、もともと @Tag("fast")
と記述していた部分をそのまま置換できます。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}
@Fast
アノテーションは次のように使用できます。
@Fast
@Test
void myFastTest() {
// ...
}
もう一歩先に進めて、 @Tag("fast")
と @Test
を置き換える @FastTest
アノテーションを定義できます。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}
JUnit は @FastTest
アノテーションで修飾したメソッドを、@Test
アノテーションと "fast" タグを指定したメソッドとして自動的に認識します。
@FastTest
void myFastTest() {
// ...
}
2.2. テストクラスとテストメソッド
テストクラス: 1つ以上の テストメソッド を含む任意のトップレベルクラス、static
宣言したメンバークラス、ネストテストクラス
抽象クラスはテストクラスにできません。 また、テストクラスに定義できるコンストラクタは1つだけです。
テストメソッド: @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate
のいずれかのアノテーションで直接、あるいは、間接的に修飾されたインスタンスメソッド
ライフサイクルメソッド: @BeforeAll, @AfterAll, @BeforeEach, @AfterEach
のいずれかのアノテーションで直接、あるいは、間接的に修飾されたインスタンスメソッド
テストメソッドとライフサイクルメソッドは、基底クラスから継承することもできますし、インターフェイスから継承することもできます(テストインターフェイスとデフォルトメソッド を参照)。
なお、どちらのメソッドも抽象メソッドにできませんし、返り値も宣言できません(ただし @TestFactory
メソッドには返り値が必要です)。
クラスとメソッドの可視性
テストクラス、テストメソッド、ライフサイクルメソッドの可視性は 他のパッケージのテストクラスを継承する場合など、技術的な必要性がない限り、基本的にはテストクラスやテストメソッドの可視性に |
次のコード例は @Test
メソッドと全てのライフサイクルメソッドを使用したものです。
実行時のセマンティクスについては テストの実行順序 と コールバックによる包み隠す振る舞い を参照してください。
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class StandardTests {
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
}
@Test
void succeedingTest() {
}
@Test
void failingTest() {
fail("a failing test");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}
@AfterEach
void tearDown() {
}
@AfterAll
static void tearDownAll() {
}
}
2.3. 表示名
テストクラスやテストメソッドには @DisplayName
で空白文字や特殊文字や絵文字などを含む独自の表示名を宣言できます。
表示名はテストレポートや IDE で実行したテストランナーで使用できます。
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("A special test case")
class DisplayNameDemo {
@Test
@DisplayName("Custom test name containing spaces")
void testWithDisplayNameContainingSpaces() {
}
@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
}
@Test
@DisplayName("😱")
void testWithDisplayNameContainingEmoji() {
}
}
2.3.1. 表示名生成ロジック
JUnit Jupiter は @DisplayNameGeneration
アノテーションで独自の表示名生成ロジックを使用できます。
ただし、DisplayNameGenerator
の生成した表示名より、@DisplayName
アノテーションで宣言した値を優先して使用します。
表示名生成ロジックを作成するときは DisplayNamaGenerator
インターフェイスを実装します。
Jupiter の提供する既存の実装は次のとおりです。
DisplayNameGenerator の実装クラス | 振る舞い |
---|---|
|
JUnit Jupiter 5.0 で導入された標準的な表示名生成ロジックです。 |
|
引数無しテストメソッドの括弧を除去します。 |
|
アンダースコアを空白文字で置換します。 |
|
エンクロージングクラスの名前と、テストクラスやテストメソッドの名前を連結して完全な文章を生成します。 |
IndicativeSentences
については、区切り文字と内部で使用する表示名生成ロジックを @IndicativeSentencesGeneration
アノテーションで設定できます。
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.IndicativeSentencesGeneration;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class DisplayNameGeneratorDemo {
@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_not_supported {
@Test
void if_it_is_zero() {
}
@DisplayName("A negative value for year is not supported by the leap year computation.")
@ParameterizedTest(name = "For example, year {0} is not supported.")
@ValueSource(ints = { -1, -4 })
void if_it_is_negative(int year) {
}
}
@Nested
@IndicativeSentencesGeneration(separator = " -> ", generator = DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_a_leap_year {
@Test
void if_it_is_divisible_by_4_but_not_by_100() {
}
@ParameterizedTest(name = "Year {0} is a leap year.")
@ValueSource(ints = { 2016, 2020, 2048 })
void if_it_is_one_of_the_following_years(int year) {
}
}
}
+-- DisplayNameGeneratorDemo [OK]
+-- A year is not supported [OK]
| +-- A negative value for year is not supported by the leap year computation. [OK]
| | +-- For example, year -1 is not supported. [OK]
| | '-- For example, year -4 is not supported. [OK]
| '-- if it is zero() [OK]
'-- A year is a leap year [OK]
+-- A year is a leap year -> if it is divisible by 4 but not by 100. [OK]
'-- A year is a leap year -> if it is one of the following years. [OK]
+-- Year 2016 is a leap year. [OK]
+-- Year 2020 is a leap year. [OK]
'-- Year 2048 is a leap year. [OK]
2.3.2. 表示名生成ロジックの初期値を変更する
junit.jupiter.displayname.generator.default
という 設定パラメータ で、JUnit Jupiter が DisplayNameGenerator
の初期値として使用するクラスの完全修飾クラス名を指定できます。
@DisplayNameGeneration
アノテーションで指定するのと同じように、DisplayNameGenerator
インターフェイスの実装クラスを指定します。
エンクロージングクラスやインターフェイスを @DisplayNameGeneration
アノテーションで修飾しない限り、常に DisplayNameGenerator
の初期値を使用します。
そして、@DisplayName
アノテーションで宣言した値は常に優先されます。
例えば、表示名生成ロジックの初期値として ReplaceUnderscores
を使うには、プロパティファイル(例えば src/test/resources/junit-platform.properties
)へ次のように記述します。
junit.jupiter.displayname.generator.default = \
org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
自分で作成した DisplayNameGenerator
インターフェイスの実装クラスも同じように指定できます。
まとめると、テストクラスやテストメソッドの表示名は次の優先順位に従って決定されます。
-
@DisplayName
アノテーションで宣言した値 -
@DisplayNameGeneration
で宣言したDisplayNameGenerator
の返す値 -
プロパティフィルで指定した
DisplayNameGenerator
の初期値が返す値 -
org.junit.jupiter.api.DisplayNameGenerator.Standard
が返す値
2.4. アサーション
JUnit Jupiter は大量のアサーションメソッドを提供します。
JUnit 4 でも提供していたメソッドに加えて、それらのメソッドを Java 8 のラムダ式で使えるようにしたメソッドが増えています。
全てのアサーションメソッドは org.junit.jupiter.api.Assertions
クラスの static
メソッドとして定義されています。
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import example.domain.Person;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class AssertionsDemo {
private final Calculator calculator = new Calculator();
private final Person person = new Person("Jane", "Doe");
@Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertEquals(4, calculator.multiply(2, 2),
"The optional failure message is now the last parameter");
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and all
// failures will be reported together.
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);
// Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}
@Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
}
@Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
new CountDownLatch(1).await();
});
}
private static String greeting() {
return "Hello, World!";
}
}
予防的なタイムアウトを
assertTimeoutPreemptively() で記述する宣言的タイムアウト と逆に、 典型的な例の1つとして、Spring Framework の提供するトランザクション管理機能が挙げられます。
特に、テストメソッドを実行する前のトランザクション状態を
|
2.4.1. Kotlin 用のアサーション
JUnit Jupiter は Kotlin から使用するためのアサーションメソッドを提供しています。
全てのアサーション関数は org.junit.jupiter.api
パッケージのトップレベル関数として定義されています。
import example.domain.Person
import example.util.Calculator
import java.time.Duration
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.assertTimeout
import org.junit.jupiter.api.assertTimeoutPreemptively
class KotlinAssertionsDemo {
private val person = Person("Jane", "Doe")
private val people = setOf(person, Person("John", "Doe"))
@Test
fun `exception absence testing`() {
val calculator = Calculator()
val result = assertDoesNotThrow("Should not throw an exception") {
calculator.divide(0, 1)
}
assertEquals(0, result)
}
@Test
fun `expected exception testing`() {
val calculator = Calculator()
val exception = assertThrows<ArithmeticException> ("Should throw an exception") {
calculator.divide(1, 0)
}
assertEquals("/ by zero", exception.message)
}
@Test
fun `grouped assertions`() {
assertAll("Person properties",
{ assertEquals("Jane", person.firstName) },
{ assertEquals("Doe", person.lastName) }
)
}
@Test
fun `grouped assertions from a stream`() {
assertAll("People with first name starting with J",
people
.stream()
.map {
// This mapping returns Stream<() -> Unit>
{ assertTrue(it.firstName.startsWith("J")) }
}
)
}
@Test
fun `grouped assertions from a collection`() {
assertAll("People with last name of Doe",
people.map { { assertEquals("Doe", it.lastName) } }
)
}
@Test
fun `timeout not exceeded testing`() {
val fibonacciCalculator = FibonacciCalculator()
val result = assertTimeout(Duration.ofMillis(1000)) {
fibonacciCalculator.fib(14)
}
assertEquals(377, result)
}
@Test
fun `timeout exceeded with preemptive termination`() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(Duration.ofMillis(10)) {
// Simulate task that takes more than 10 ms.
Thread.sleep(100)
}
}
}
2.4.2. 外部のアサーションライブラリ
Junit Jupiter はたいていのテストを満足するアサーションの部品を提供していますが、マッチャー のようにより強力で便利な機能が必要になる場合もあります。 そういう場合、JUnit チームとしては AssertJ や Hamcrest や Truth などの外部ライブラリを使用することをお勧めしています。 開発者は自由にアサーションライブラリを選択できるのです。
例えば、マッチャー を組み合わせたり、フルーエント API を使うことで、可読性や説明力の高いアサーションを表現できるようになります。
JUnit 4 の org.junit.Assert
クラスが提供する
assertThat()
は、
Hamcrest のマッチャー
を組み合わせて使用できるようになっていました。
しかし、JUnit Jupiter の org.junit.jupiter.api.Assertions
クラスは assertThat()
を提供していないため、開発者は外部アサーションライブラリの提供するマッチャーを使わなければなりません。
次のコード例では、JUnit Jupiter のテストで Hamcrest の提供する assertThat()
を使っています。
Hamcrest の jar ファイルがクラスパスに存在するなら、assertThat(), is(), equalTo()
などの static
メソッドを import して、assertWithHamcrestMatcher()
テストメソッドで使用できます。
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class HamcrestAssertionsDemo {
private final Calculator calculator = new Calculator();
@Test
void assertWithHamcrestMatcher() {
assertThat(calculator.subtract(4, 1), is(equalTo(3)));
}
}
JUnit 4 のプログラミングモデルで作成したテストなら引き続き org.junit.Assert#assertThat
メソッドを使用できます。
2.5. アサンプション(仮定)
JUnit Jupiter では、JUnit 4 で提供していたアサンプションメソッドの一部と、それらのメソッドを Java 8 のラムダ式でメソッド参照として使用できるようにしたメソッドを提供しています。
全てのアサンプションメソッドは org.junit.jupiter.api.Assumptions
クラスの static
メソッドとして定義されています。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class AssumptionsDemo {
private final Calculator calculator = new Calculator();
@Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
}
@Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// remainder of test
}
@Test
void testInAllEnvironments() {
assumingThat("CI".equals(System.getenv("ENV")),
() -> {
// perform these assertions only on the CI server
assertEquals(2, calculator.divide(4, 2));
});
// perform these assertions in all environments
assertEquals(42, calculator.multiply(6, 7));
}
}
JUnit Jupiter 5.4 から JUnit 4 の org.junit.Assume クラスのメソッドをアサンプションメソッドとして使えるようになりました。
例えば、JUnit 4 の AssumptionViolatedException で、本来失敗とするべきテストを中断できるようになっています。
|
2.6. テストを無効にする
条件に基づくテスト実行 で紹介した @Disabled
アノテーションで修飾したり、独自の ExecutionCondition
を作成したりすることで、テストクラス全体あるいは一部のテストメソッドだけを 無効 にできます。
次のコード例では @Disabled
でテストクラスを無効にしています。
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
次のコード例では @Disabled
でテストメソッドを無効にしています。
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class DisabledTestsDemo {
@Disabled("Disabled until bug #42 has been resolved")
@Test
void testWillBeSkipped() {
}
@Test
void testWillBeExecuted() {
}
}
理由(reason) を指定せずに @Disabled を記述する場合もあると思います。
しかし、JUnit チームとしては、簡単でいいからテストクラスやテストメソッドを無効にする理由を書いたほうがいいと考えています。
コード例では @Disabled("Disabled until bug \#42 has been resolved") のように理由を書いています。
開発チームによっては、課題管理ツールに登録した課題を自動的に追跡できるよう、課題番号を書いている場合もあるそうです。
|
2.7. 条件に基づくテスト実行
JUnit Jupiter の ExecutionCondition
API を使うと、手続き的に評価した条件 に応じて、テストコンテナやテストの 有効 と 無効 を制御できます。
組み込みの ExecutionCondition
の中でも分かりやすいのは、@Disabled
アノテーション(テストを無効にする)に対応する DisabledCondition
です。
org.junit.jupiter.api.condition
パッケージには、@Disabled
以外のさまざまなアノテーションに対応する ExecutionCondition
があるため、開発者は 宣言的に テストクラスやテストメソッドの 有効 や 無効 を制御できます。
複数の ExecutionCondition
を登録した場合、どれか1つでも条件を満たせばテストコンテナやテストは直ちに 無効 になります。
組み込みの ExecutionCondition
に対応する全てのアノテーションには disabledReason
という属性があるため、テストを無効にした理由を提供できます。
ExecutionCondition
や、次のセクションも参照してください。
合成アノテーション
次のセクションで説明している、条件として使用できる 全てのアノテーションは、合成アノテーション を作るためのメタアノテーションとして使用できます。
例えば、 @EnabledOnOs デモ では |
特別な記載がない限り、次のセクションで説明している 条件として使用できる 全てのアノテーションは1度しか指定できません。
直接的に指定するだけでなく、継承やメタアノテーションにより間接的に指定する場合も含めて、JUnit は最初に発見したアノテーションだけを使用します。
他のアノテーションは無視されてしまうのです。
しかし、 |
2.7.1. OS の条件指定
@EnabledOnOs
や @DisabledOnOs
を使うと、指定した OS でテストコンテナやテストの有効と無効を制御できます。
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
// ...
}
@TestOnMac
void testOnMac() {
// ...
}
@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
// ...
}
@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
// ...
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs(MAC)
@interface TestOnMac {
}
2.7.2. Java 実行環境(JRE:Java Runtime Environment) の条件指定
@EnabledOnJre
や @DisabledOnJre
を使うと、指定した JRE でテストコンテナやテストの有効と無効を制御できます。
@EnabledForJreRange
や @DisabledForJreRange
を使うと、JRE のバージョンを範囲指定できます。
範囲指定の最小値(min
)の初期値は JRE.JAVA_8
で、最大値(max
)の初期値は JRE.OTHER
です。
範囲は半開区間として評価します。
@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
// ...
}
@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9to11() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9)
void fromJava9toCurrentJavaFeatureNumber() {
// ...
}
@Test
@EnabledForJreRange(max = JAVA_11)
void fromJava8To11() {
// ...
}
@Test
@DisabledOnJre(JAVA_9)
void notOnJava9() {
// ...
}
@Test
@DisabledForJreRange(min = JAVA_9, max = JAVA_11)
void notFromJava9to11() {
// ...
}
@Test
@DisabledForJreRange(min = JAVA_9)
void notFromJava9toCurrentJavaFeatureNumber() {
// ...
}
@Test
@DisabledForJreRange(max = JAVA_11)
void notFromJava8to11() {
// ...
}
2.7.3. システムプロパティの条件指定
@EnabledIfSystemProperty
や @DisabledIfSystemProperty
を使うと、指定したシステムプロパティでテストコンテナやテストの有効と無効を制御できます。
matches
属性に指定した値は正規表現として評価します。
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
// ...
}
@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
// ...
}
JUnit Jupiter 5.6 から、 |
2.7.4. 環境変数の条件指定
@EnabledIfEnvironmentVariable
や @DisabledIfEnvironmentVariable
を使うと、指定したシステムプロパティでテストコンテナやテストの有効と無効を制御できます。
matches
属性に指定した値は正規表現として評価します。
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
// ...
}
@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {
// ...
}
JUnit Jupiter 5.6 から、 |
2.7.5. 独自の条件指定
@EnabledIf
や @DisabledIf
を使うと、指定した名前のメソッドを実行したときの返り値に応じて、テストコンテナやテストの有効と無効を制御できます。
指定するメソッドには単一の引数 ExtensionContext
を渡すことができます。
@Test
@EnabledIf("customCondition")
void enabled() {
// ...
}
@Test
@DisabledIf("customCondition")
void disabled() {
// ...
}
boolean customCondition() {
return true;
}
一方、テストクラス以外の場所で定義されたメソッドを指定することもできます。 メソッド名は 完全修飾名 で指定しなければなりません。
package example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
class ExternalCustomConditionDemo {
@Test
@EnabledIf("example.ExternalCondition#customCondition")
void enabled() {
// ...
}
}
class ExternalCondition {
static boolean customCondition() {
return true;
}
}
テストクラスに @EnabledIf や @EnabledIf を指定するときに指定できるメソッドは static 宣言しなければなりません。
テストクラス以外の場所で定義されたメソッドを指定するとして、そのメソッドも static 宣言しなければなりません。
それ以外の場合は static 宣言したメソッドと、インスタンスメソッドのどちらでも利用できます。
|
2.8. タグ付けと選択
テストクラスやテストメソッドには @Tag
アノテーションでタグ付けが可能です。
タグは テストを検出して実行 するときにテストを選択するための条件に使用できます。
タグ セクションでは JUnit Platform のタグ機能について説明しています。
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("fast")
@Tag("model")
class TaggingDemo {
@Test
@Tag("taxes")
void testingTaxCalculation() {
}
}
タグ付けのためのアノテーションを作成する方法については メタアノテーションと合成アノテーション を参照してください。 |
2.9. テストの実行順序
初期設定では、テストクラスやテストメソッドの実行順序が、意図的に曖昧にした決定論的なアルゴリズムで決まるようになっています。 テストスイートが実行するテストクラスやテストメソッドの順番は変わらないことが保証されているため、繰り返し可能なビルド(repeatable build)を推進してくれます。
テストクラス や テストメソッド の定義については テストクラスとテストメソッド を参照してください。 |
2.9.1. テストメソッドの実行順序
本物の ユニットテスト は普通ならどのような順番で実行しても同じ結果になるべきだと言われています。
しかし、どうしても実行する順番を指定しなければならないこともあります。
例えば、統合テスト や 機能テスト を作成するときは順番が重要です。
特に @TestInstance(Lifecycle.PER_CLASS)
も組み合わせているならなおのことです。
テストメソッドを実行する順番を制御するには、テストクラスやテストインターフェイスを @TestMethodOrder
アノテーションで修飾し、適切な MethodOrderer
の実装を指定します。
MethodOrderer
インターフェイスを自分で実装することもできますし、組み込みの実装を使うこともできます。
-
MethodOrderer.DisplayName
: テストメソッドの表示名に基づいて 英数字の昇順 に並び替えます。表示名の決定規則 も参照してください -
MethodOrderer.MethodName
: テストメソッドの名前と形式的な引数リストの名前に基づいて 英数字の昇順 に並び替えます。 -
MethodOrderer.OrderAnnotation
:@Order
アノテーションに指定した整数値に基づいて 数字の昇順 に並び替えます。 -
MethodOrderer.Random
: 疑似乱数 に基づいて並び替えます。乱数の シード を指定できます。 -
MethodOrderer.Alphanumeric
: テストメソッドの名前と形式的な引数リストの名前に基づいて 英数字の昇順 に並び替えます。MethodOrderer.MethodName
を導入したため非推奨になりました。6.0 をリリースするときに削除されます。
コールバックによる包み隠す振る舞い も参照してください。 |
次のコード例は @Order
アノテーションでテストメソッドの実行順序を指定しています。
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(OrderAnnotation.class)
class OrderedTestsDemo {
@Test
@Order(1)
void nullValues() {
// perform assertions against null values
}
@Test
@Order(2)
void emptyValues() {
// perform assertions against empty values
}
@Test
@Order(3)
void validValues() {
// perform assertions against valid values
}
}
テストメソッドの実行順序の初期値を変更する
junit.jupiter.testmethod.order.default
という 設定パラメータ で、JUnit Jupiter が初期値として使用する MethodOrderer
の実装クラスの完全修飾クラス名を指定できます。
エンクロージングクラスやインターフェイスを @TestMethodOrder
アノテーションで修飾しない限り、常に MethodOrderer
の初期値を使用します。
そして、@TestMethodOrder
アノテーションで指定した順序決定ルールは常に優先されます。
初期値を MethodOrderer.OrderAnnotation
へ変更するには、プロパティファイル(例えば src/test/resources/junit-platform.properties
)へ次のように記述します。
junit.jupiter.testmethod.order.default = \
org.junit.jupiter.api.MethodOrderer$OrderAnnotation
自分で作成した MethodOrderer
インターフェイスの実装クラスも同じように指定できます。
2.9.2. テストクラスの実行順序
普通ならテストクラスをどのような順番で実行しても同じ結果になるべきだと言われています。 しかし、どうしても実行する順番を指定しなければならないこともあります。 テストクラス間に意図しない依存関係は存在しないことを保証するため、テストクラスをランダムな順序で実行したい場合もあるでしょう。 また、ビルド時間を最適化するため、実行順序を指定したい場合もあるでしょう。
-
前回失敗したテストを先に実行したい: フェールファストモード
-
並列実行できるなら時間のかかるテストを先に実行したい: テスト実行時間最小化モード
-
それ以外のさまざまなユースケース
テストスイートの 大域的な テストクラスの実行順序は、junit.jupiter.testclass.order.default
という 設定パラメータ で、JUnit Jupiter が初期値として使用する ClassOrderer
の実装クラスの完全修飾クラス名を指定できます。
自分で作成した ClassOrderer
インターフェイスの実装クラスも同じように指定できます。
-
ClassOrderer.ClassName
: テストクラスの完全修飾クラス名に基づいて 英数字の昇順 に並び替えます。 -
ClassOrderer.DisplayName
: テストクラスの表示名に基づいて 英数字の昇順 に並び替えます。表示名の決定規則 も参照してください -
ClassOrderer.OrderAnnotation
:@Order
アノテーションに指定した整数値に基づいて 数字の昇順 に並び替えます。 -
ClassOrderer.Random
: 疑似乱数 に基づいて並び替えます。乱数の シード を指定できます。
次のコード例は @Order
アノテーションで テストクラス の実行順序を指定しています。
初期値を ClassOrderer.OrderAnnotation
へ変更するには、プロパティファイル(例えば src/test/resources/junit-platform.properties
)へ次のように記述します。
junit.jupiter.testclass.order.default = \
org.junit.jupiter.api.ClassOrderer$OrderAnnotation
ClassOrder
は、全てのトップレベルテストクラス(static
宣言したネストテストクラスも含む)と @Nested
で修飾したネストテストクラスに指定できます。
トップレベルテストクラスの順序は同じトップレベルテストクラス同士で比較します。同じエンクロージングクラスのネストテストクラスの順序は、同じエンクロージングクラスのネストテストクラス同士で比較します。 |
ネストテストクラスの 局所的な 実行順序を指定するには、エンクロージングクラスを @TestClassOrder
で修飾します。
そして @TestClassOrder
アノテーションに ClassOrder
を実装したクラスのクラス参照を直接指定します。
ClassOrder
は再帰的にネストテストクラスを探索します。
@TestClassOrder
の局所的な宣言は、基底クラスから継承した @TestClassOrder
の宣言で上書きされます。
また、設定パラメータ junit.jupiter.testclass.order.default
で大域的に宣言した ClassOrder
で上書きされます。
次のコード例は @Order
アノテーションで ネストテストクラス の実行順序を指定しています。
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class OrderedNestedTestClassesDemo {
@Nested
@Order(1)
class PrimaryTests {
@Test
void test1() {
}
}
@Nested
@Order(2)
class SecondaryTests {
@Test
void test2() {
}
}
}
2.10. テストインスタンスのライフサイクル
それぞれのテストメソッドが独立して実行できるよう、そして、変更可能なテストインスタンスの状態に基づく予期せぬ副作用を無くすため、JUnit は テストメソッド (テストクラスとテストメソッド)を実行するたびに、テストクラスの新しいインスタンスを作るようにしました。 この、メソッド単位(per-method)のインスタンスライフサイクル管理は、JUnit Jupiter の基本的な振る舞いであり、過去の全ての JUnit と同じものです。
メソッド単位(per-method)のインスタンスライフサイクル管理が有効になっているなら、テストメソッドが 無効 になっても、テストクラスのインスタンスは作成するので注意してください(条件に基づくテスト実行 や @Disabled アノテーションなど)。
|
JUnit Jupiter に同じインスタンスで全てのテストメソッドを実行させたいときは、テストクラスを @TestInstance(Lifecycle.PER_CLASS)
で修飾しましょう。
そうすると、テストクラスのインスタンスを1度しか作らなくなります。
テストメソッドがインスタンス変数に記録した状態へ依存しているなら、@BeforeEach
や @AfterEach
のメソッドで状態を初期化しなければなりません。
クラス単位(per-class)のインスタンスライフサイクル管理にはメソッド単位(per-method)にない利点があります。
それは、@BeforeAll
や @AfterAll
のメソッドに static
宣言が不要になることです(同様に、インターフェイスの default
メソッドも不要になります)。
クラス単位(per-class)のインスタンスライフサイクル管理では、ネストテストクラスで @BeforeAll
と @AfterAll
を使うこともできます。
Kotlin でテストを書いているときも、クラス単位(per-class)のインスタンスライフサイクル管理 にしたほうが、@BeforeAll
や @AfterAll
をメソッドで実装できるので便利でしょう。
2.10.1. インスタンスライフサイクル管理の初期値を変更する
テストクラスやテストインターフェイスを @TestInstance
で修飾していないとき、JUnit Jupiter は 既定 のインスタンスライフサイクル管理をします。
本来の 初期値 は PER_METHOD
ですが、テスト実行計画全体で変更できます。
インスタンスライフサイクル管理の初期値を変更するには、junit.jupiter.testinstance.lifecycle.default
という 設定パラメータ に、TestInstance.Lifecycle
の列挙値を設定します(大文字小文字の違いは無視します)。
JVM のシステムプロパティや、Launcher
に渡す LauncherDiscoveryRequest
の 設定パラメータ 、JUnit Platform の設定ファイル(設定パラメータの構成 を参照)で指定することもできます。
システムプロパティでインスタンスライフサイクル管理の初期値に Lifecycle.PER_CLASS
を設定するには次のように指定します。
-Djunit.jupiter.testinstance.lifecycle.default=per_class
いろいろ指定する方法あありますが、JUnit Platform の設定ファイルで指定するのが最も確実な方法です。 JUnit Platform の設定ファイルはソースコードリポジトリに格納してバージョン管理して、IDE やビルドツールから利用できるようにするといいでしょう。、
JUnit Platform の設定ファイルでインスタンスライフサイクル管理の初期値に Lifecycle.PER_CLASS
を設定するには次のように記述します(例えば rsc/test/resources/junit-platform.properties
)。
junit.jupiter.testinstance.lifecycle.default = per_class
インスタンスライフサイクル管理の初期値を不適切な値に変更すると、テストの結果が予測不能になったり、不安定になったりします。 例えば、ビルドツールの初期値をクラス単位(per-class)にしたのに、IDE ではメソッド単位(per-method)でテストを実行していると、ビルドジョブで解析の難しいエラーが発生します。 それゆえに、システムプロパティではなく JUnit Platform の設定ファイルで初期値を変更することをお勧めします。 |
2.11. ネストテストクラス
@Nested
アノテーションを使うと、いろいろなグループのテストの関係性をより分かりやすく表現できるようになります。
Java の内部クラスでテストの階層構造を考えられるようになるからです。
次のコード例をIDEで実行した結果は次のとおりです。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
このテストを IDE で実行すると、実行結果は同じような木構造になります。
この場合、内側のテストで階層的なライフサイクルメソッドを定義することで、外側のテストの事前条件を内側のテストで使用しているのが分かります。
createNewStack()
は @BeforeEach
で修飾されたライフサイクルメソッドで、メソッドを定義したクラスと、その内側のクラスのテストメソッドを実行するときに使用します。
外側のテストのセットアップは内側のテストより先に実行するため、結果として全てのテストを独立して実行できることになります。 内側のテストを実行するときは常に外側のテストのセットアップを実行するので、内側のテストだけを実行できるのです。
@Nested で修飾できるのは static でない内部クラス(インナークラス) だけです。
どれだけネストが深くなったとしても内部クラスか1つの例外を除いて完全なライフサイクルに対応しています。
例外とは、@BeforeAll と @AfterAll が 初期設定 では動作しないことです。
Java の内部クラスは static メンバを定義できないからです。
ただし、ネストテストクラスを @TestInstance(Lifecycle.PER_CLASS) で修飾すればその例外を回避できます(テストインスタンスのライフサイクル を参照)。
|
2.12. コンストラクタやメソッドによる依存性の注入
過去の JUnit ではテストクラスのコンストラクタやテストメソッドに引数を定義できませんでした(少なくとも標準の Runner
ではそうでした)。
JUnit Jupiter における重要な変更点の1つは、テストクラスのコンストラクタやテストメソッドに引数を定義できるようになったことです。
優れた柔軟性をもたらすだけでなく、依存性の注入 ができるようになったのです。
ParameterResolver
は JUnit の拡張機能が実行時に 動的に パラメータを解決するための API を定義しています。
ParameterResolver
を登録しておくと、実行時に テストクラス のコンストラクタや テストメソッド あるいは ライフサイクルメソッド の引数を解決してくれるのです(テストクラスとテストメソッド を参照)。
現在は次の3種類の組み込みリゾルバーが自動的に登録されるようになっています。
-
TestInfoParameterResolver
: コンストラクタやメソッドにTestInfo
型の引数があれば、TestInfoParameterResolver
が今のテストコンテナあるいはテストの情報を持つTestInfo
のインスタンスを設定します。 設定されたTestInfo
から、今のテストコンテナやテストの情報(例えば表示名、テストクラス名、テストメソッド名、割り当てられたタグなど)を取得できます。 表示名はクラス名やメソッド名、あるいは、@DisplayName
で指定された名前になります。TestInfo
は JUnit 4 のTestName
ルールを置き換える部品として使用できます。 次のコード例ではテストクラスのコンストラクタと@BeforeEach
メソッド、テストメソッドにTestInfo
を注入しています。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
@DisplayName("TestInfo Demo")
class TestInfoDemo {
TestInfoDemo(TestInfo testInfo) {
assertEquals("TestInfo Demo", testInfo.getDisplayName());
}
@BeforeEach
void init(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
}
@Test
@DisplayName("TEST 1")
@Tag("my-tag")
void test1(TestInfo testInfo) {
assertEquals("TEST 1", testInfo.getDisplayName());
assertTrue(testInfo.getTags().contains("my-tag"));
}
@Test
void test2() {
}
}
-
RepetitionInfoParameterResolver
:@RepeatedTest, @BeforeEach, @AfterEach
で修飾したメソッドにRepetitionInfo
型の引数があれば、RepetitionInfoParameterResolver
がRepetitionInfo
のインスタンスを設定します。 設定されたRepetitionInfo
から、今の繰り返し情報と@RepeatedTest
に指定された繰り返し数の合計値を取得できます。@RepeatedTest
の外側のコンテキストではRepetitionInfoParameterResolver
を登録しないので注意してください。繰り返しテストの具体例 も参照してください。 -
TestReporterParameterResolver
: コンストラクタやメソッドにTestReporter
型の引数があれば,TestReporterParameterResolver
がTestReporter
のインスタンスを設定します。 設定されたTestReporter
で、実行しているテストに関するデータを公開できます。 公開したデータはTestExecutionListener
のreportingEntryPublished()
メソッドで取得できるので、IDE に表示したりレポートに出力したりできます。JUnit 4 で標準出力と標準エラー出力へテストの結果を出力していた場合、JUnit Jupiter では
TestReporter
を使わなければいけません。 テストクラスを@RunWith(JUnitPlatform.class)
で修飾すると、全ての出力を標準出力へ書き込むようになります。 また、IDE によっては、テストの結果を標準出力と UI の両方に出力します。
class TestReporterDemo {
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a status message");
}
@Test
void reportKeyValuePair(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
@Test
void reportMultipleKeyValuePairs(TestReporter testReporter) {
Map<String, String> values = new HashMap<>();
values.put("user name", "dk38");
values.put("award year", "1974");
testReporter.publishEntry(values);
}
}
他のパラメータリゾルバーを使いたければ適切な拡張機能を @ExtendWith で登録しなければなりません。
|
ParameterResolver
の実装例として RandomParametersExtension
を見てみましょう。
拡張モデルとパラメータ解決処理を説明するため、短く、分かりやすくしています。
また、MyRandomParametersTest
でその使い方を説明します。
@ExtendWith(RandomParametersExtension.class)
class MyRandomParametersTest {
@Test
void injectsInteger(@Random int i, @Random int j) {
assertNotEquals(i, j);
}
@Test
void injectsDouble(@Random double d) {
assertEquals(0.0, d, 1.0);
}
}
現実的な例は MockitoExtension
や SpringExtension
のソースコードを参照してください。
作成した ParameterResolver
が引数の型だけで値を注入するかどうかを判断するなら、総称型の TypeBasedParameterResolver
を基底クラスにするといいでしょう。
supportsParameters
メソッドが対応する引数の型の情報を隠蔽します。
2.13. テストインターフェイスとデフォルトメソッド
JUnit Jupiter ではインターフェイスのデフォルトメソッドを @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate, @BeforeEach, @AfterEach
で修飾できるようになりました。
インターフェイスやクラスを @TestInstance(Lifecycle.PER_CLASS)
(テストインスタンスのライフサイクル を参照)で修飾しているなら、static
メソッドやインターフェイスのデフォルトメソッドを @BeforeAll, @AfterAll
で修飾できます。
@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {
static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());
@BeforeAll
default void beforeAllTests() {
logger.info("Before all tests");
}
@AfterAll
default void afterAllTests() {
logger.info("After all tests");
}
@BeforeEach
default void beforeEachTest(TestInfo testInfo) {
logger.info(() -> String.format("About to execute [%s]",
testInfo.getDisplayName()));
}
@AfterEach
default void afterEachTest(TestInfo testInfo) {
logger.info(() -> String.format("Finished executing [%s]",
testInfo.getDisplayName()));
}
}
interface TestInterfaceDynamicTestsDemo {
@TestFactory
default Stream<DynamicTest> dynamicTestsForPalindromes() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
}
テストインターフェイスの実装クラスは、テストインターフェイスを修飾した @ExtendWith, @Tag
を自動的に継承します。
テスト実行の前、および、後のコールバック の実装例を、TimingExtension のソースコードで確認しましょう。
@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}
これらのインターフェイスをテストクラスで実装すると次のようになります。
class TestInterfaceDemo implements TestLifecycleLogger,
TimeExecutionLogger, TestInterfaceDynamicTestsDemo {
@Test
void isEqualValue() {
assertEquals(1, "a".length(), "is always equal");
}
}
TestInterfaceDemo
を実行すると次のような出力が得られるでしょう。
INFO example.TestLifecycleLogger - Before all tests INFO example.TestLifecycleLogger - About to execute [dynamicTestsForPalindromes()] INFO example.TimingExtension - Method [dynamicTestsForPalindromes] took 19 ms. INFO example.TestLifecycleLogger - Finished executing [dynamicTestsForPalindromes()] INFO example.TestLifecycleLogger - About to execute [isEqualValue()] INFO example.TimingExtension - Method [isEqualValue] took 1 ms. INFO example.TestLifecycleLogger - Finished executing [isEqualValue()] INFO example.TestLifecycleLogger - After all tests
この機能を応用して、インターフェイスを契約とするテストを作成できます。
例えば、Object.equals
や Comparable.compareTo
の実装に関するテストは次のように記述できるでしょう。
public interface Testable<T> {
T createValue();
}
public interface EqualsContract<T> extends Testable<T> {
T createNotEqualValue();
@Test
default void valueEqualsItself() {
T value = createValue();
assertEquals(value, value);
}
@Test
default void valueDoesNotEqualNull() {
T value = createValue();
assertFalse(value.equals(null));
}
@Test
default void valueDoesNotEqualDifferentValue() {
T value = createValue();
T differentValue = createNotEqualValue();
assertNotEquals(value, differentValue);
assertNotEquals(differentValue, value);
}
}
public interface ComparableContract<T extends Comparable<T>> extends Testable<T> {
T createSmallerValue();
@Test
default void returnsZeroWhenComparedToItself() {
T value = createValue();
assertEquals(0, value.compareTo(value));
}
@Test
default void returnsPositiveNumberWhenComparedToSmallerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(value.compareTo(smallerValue) > 0);
}
@Test
default void returnsNegativeNumberWhenComparedToLargerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(smallerValue.compareTo(value) < 0);
}
}
それぞれのインターフェイスをテストクラスで実装すると、それぞれのテストを継承することになります。 もちろん、インターフェイスメソッドは実装しなければなりません。
class StringTests implements ComparableContract<String>, EqualsContract<String> {
@Override
public String createValue() {
return "banana";
}
@Override
public String createSmallerValue() {
return "apple"; // 'a' < 'b' in "banana"
}
@Override
public String createNotEqualValue() {
return "cherry";
}
}
これらのコード例は説明用で、完全なものではありません。 |
2.14. 繰り返しテスト
JUnit Jupiter では @RepeatedTest
でテストを繰り返し実行する回数、あるいは、繰り返し回数の最大値を指定してテストを実行できるようになりました。
繰り返しごとに実行するテストメソッドは、@Test
で修飾した通常のテストメソッドと完全に同じライフサイクルと拡張機能に対応しています。
次のコード例では、repeatedTest()
を10回繰り返すようになっています。
@RepeatedTest(10)
void repeatedTest() {
// ...
}
繰り返し実行する回数を指定するだけでなく、name
属性で表示名を変更できるようになっています。
表示名は、文字列リテラルとプレースホルダを組み合わせたパターン文字列です。
現在対応しているプレースホルダは次のとおりです。
-
{displayName}
:@RepeatedTest
で修飾したメソッド名 -
{currentRepetition}
: 現在の繰り返し回数 -
{totalRepetitions}
: 繰り返し回数の最大値
表示名の初期値は "repetition {currentRepetition} of {totalRepetitions}"
です。
従って、前のコード例では繰り返しごとの表示名は repetition 1 of 10
、 repetition 2 of 10
のようになります。
それぞれの表示名にテストメソッド名を含めたい場合は、独自のパターン文字列を指定するか、定義済みの RepeatedTest.LONG_DISPLAY_NAME
を指定するといいでしょう。
RepeatedTest.LONG_DISPLAY_NAME
は "{displayName} :: repetition {currentRepetition} of {totalRepetitions}"
と同じ内容で、repeatedTest() :: repetition 1 of 10
、 repeatedTest() :: repetition 2 of 10
のように出力されます。
@RepeatedTest, @BeforeEach, @AfterEach
で修飾したメソッドの内部から、現在の繰り返し回数や、繰り返し回数の最大値を取得したい場合、メソッド引数に RepetitionInfo
型の引数を宣言し、インスタンスを注入させることができます。
2.14.1. 繰り返しテストの具体例
繰り返しテストのさまざまな使い方を RepeatedTestsDemo
クラスで説明していきます。
repeatedTest()
メソッドの使い方は前のセクションで説明したのと同じです。
repeatedTestWithRepetitionInfo()
メソッドは RepetitionInfo
でメソッドの中から繰り返し回数などの情報を取得しています。
customDisplayName()
メソッドは @DisplayName
と @RepeatedTest
で変更した表示名を TestInfo
から取得して、生成された名前の形式が合っているか検証しています。
Repeat!
はプレースホルダの {displayName}
を @DisplayName
の値で置換したものです。
1/1
はプレースホルダの {currentRepetition}/{totalRepetitions}
を置換したものです。
逆に customDisplayNameWithLongPattern()
メソッドでは表示名のパターンとして RepeatedTest.LONG_DISPLAY_NAME
を使っています。
repeatedTestInGerman()
メソッドは繰り返しテストで表示名をローカライズする方法を示しています。
ここではドイツ語へ翻訳しているので、Wiederholung 1 von 5
, Wiederholung 2 von 5
のようになります。
@BeforeEach
で修飾した beforeEach()
メソッドは、それぞれの繰り返しテストを実行する前に実行します。
メソッド引数に注入した TestInfo
と RepetitionInfo
から、実行しているテストメソッドの情報を取得できます。
ログレベルを INFO
に設定してテストクラス RepeatedTestsDemo
を実行すると、次のような出力が得られるでしょう。
INFO: About to execute repetition 1 of 10 for repeatedTest INFO: About to execute repetition 2 of 10 for repeatedTest INFO: About to execute repetition 3 of 10 for repeatedTest INFO: About to execute repetition 4 of 10 for repeatedTest INFO: About to execute repetition 5 of 10 for repeatedTest INFO: About to execute repetition 6 of 10 for repeatedTest INFO: About to execute repetition 7 of 10 for repeatedTest INFO: About to execute repetition 8 of 10 for repeatedTest INFO: About to execute repetition 9 of 10 for repeatedTest INFO: About to execute repetition 10 of 10 for repeatedTest INFO: About to execute repetition 1 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 2 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 3 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 4 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 5 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 1 of 1 for customDisplayName INFO: About to execute repetition 1 of 1 for customDisplayNameWithLongPattern INFO: About to execute repetition 1 of 5 for repeatedTestInGerman INFO: About to execute repetition 2 of 5 for repeatedTestInGerman INFO: About to execute repetition 3 of 5 for repeatedTestInGerman INFO: About to execute repetition 4 of 5 for repeatedTestInGerman INFO: About to execute repetition 5 of 5 for repeatedTestInGerman
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.logging.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;
class RepeatedTestsDemo {
private Logger logger = // ...
@BeforeEach
void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
int currentRepetition = repetitionInfo.getCurrentRepetition();
int totalRepetitions = repetitionInfo.getTotalRepetitions();
String methodName = testInfo.getTestMethod().get().getName();
logger.info(String.format("About to execute repetition %d of %d for %s", //
currentRepetition, totalRepetitions, methodName));
}
@RepeatedTest(10)
void repeatedTest() {
// ...
}
@RepeatedTest(5)
void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
assertEquals(5, repetitionInfo.getTotalRepetitions());
}
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
assertEquals("Repeat! 1/1", testInfo.getDisplayName());
}
@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
assertEquals("Details... :: repetition 1 of 1", testInfo.getDisplayName());
}
@RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
void repeatedTestInGerman() {
// ...
}
}
ユニコードテーマを有効にした ConsoleLauncher
を使うと次のような出力が得られるでしょう。
├─ RepeatedTestsDemo ✔ │ ├─ repeatedTest() ✔ │ │ ├─ repetition 1 of 10 ✔ │ │ ├─ repetition 2 of 10 ✔ │ │ ├─ repetition 3 of 10 ✔ │ │ ├─ repetition 4 of 10 ✔ │ │ ├─ repetition 5 of 10 ✔ │ │ ├─ repetition 6 of 10 ✔ │ │ ├─ repetition 7 of 10 ✔ │ │ ├─ repetition 8 of 10 ✔ │ │ ├─ repetition 9 of 10 ✔ │ │ └─ repetition 10 of 10 ✔ │ ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔ │ │ ├─ repetition 1 of 5 ✔ │ │ ├─ repetition 2 of 5 ✔ │ │ ├─ repetition 3 of 5 ✔ │ │ ├─ repetition 4 of 5 ✔ │ │ └─ repetition 5 of 5 ✔ │ ├─ Repeat! ✔ │ │ └─ Repeat! 1/1 ✔ │ ├─ Details... ✔ │ │ └─ Details... {two-colons} repetition 1 of 1 ✔ │ └─ repeatedTestInGerman() ✔ │ ├─ Wiederholung 1 von 5 ✔ │ ├─ Wiederholung 2 von 5 ✔ │ ├─ Wiederholung 3 von 5 ✔ │ ├─ Wiederholung 4 von 5 ✔ │ └─ Wiederholung 5 von 5 ✔
2.15. パラメタライズテスト
パラメタライズテストは複数の異なる引数でテストメソッドを実行する機能です。
テストメソッドを @Test
ではなく @ParameterizedTest
で修飾します。
メソッドの引数を提供する ソース を少なくとも1つは宣言しなければなりません。
テストメソッドは ソース の提供するそれぞれの値を引数として 消費 します。
次のコード例では @ValueSource
で String
の配列を与えています。
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
パラメタライズテストを実行すると、それぞれのテストメソッドの呼び出しを別々に報告します。
例えば、ConsoleLauncher
は次のように出力するでしょう。
palindromes(String) ✔ ├─ [1] candidate=racecar ✔ ├─ [2] candidate=radar ✔ └─ [3] candidate=able was I ere I saw elba ✔
2.15.1. 必要な準備
パラメタライズテストを実行するには、依存ライブラリに junit-jupiter-params
を追加しなければなりません。
詳しくは 依存ライブラリに関するメタデータ を参照してください。
2.15.2. 引数を消費するということについて
パラメタライズテストでは基本的に指定した ソース から直接値を 消費 します。
このとき、ソース から取得した値の(配列の)添え字と、メソッド引数の順序の対応関係は1対1になります(@CsvSource を参照)。
しかし、ソース から取得した値を 集約 して、メソッド引数の1つに渡すこともできます(引数の集約 を参照)。
また、メソッド引数には ParameterResolver
が注入する引数(例えば TestInfo
や TestReporter
など)を宣言できます。
パラメタライズテストにおけるメソッド引数の宣言規則は次のとおりです。
-
0 個以上の 添え字に対応する引数 は先頭で宣言しなければなりません。
-
0 個以上の 集約する引数 はその次に宣言しなければなりません。
-
0 個以上の
ParameterResolver
が注入する引数は最後に宣言しなければなりません。
「添え字に対応する引数」とは、ArgumentsProvider
の提供する Arguments
に対する添え字のことで、添え字に対応する引数へ値を渡します。
「集約する引数」とは、ArgumentAccessor
型の引数か、@AggregateWith
で修飾した引数のことです。
AutoCloseable 型の引数について
この振る舞いを無効化するには |
2.15.3. 引数の源
JUnit Jupiter はいくつか ソース のアノテーションを同梱しています。
このセクションではそれぞれのアノテーションについて簡単な使い方を説明してきます。
詳細は org.junit.jupiter.params.provider
パッケージの Javadoc を参照してください。
@ValueSource
@ValueSource
はソースアノテーションの中でも簡単なものの1つです。
配列やリテラル値を、1引数のテストメソッドに渡すことができます。
@ValueSource
の対応しているリテラルのデータ型は次のとおりです。
-
short
-
byte
-
int
-
long
-
float
-
double
-
char
-
boolean
-
java.lang.String
-
java.lang.Class
例えば、次のコード例では 1, 2, 3
それぞれを引数として @ParameterizedTest
で修飾したテストメソッドを実行します。
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
assertTrue(argument > 0 && argument < 4);
}
null および空値
コーナーケースや 不正な入力 を与えられたときの振る舞いを確かめるには、パラメタライズテストで null
や 空値 を渡せるようにすると便利です。
1引数のテストメソッドへ null
や空値を渡すためのアノテーションには次のようなものがあります。
-
@NullSource
:@ParameterizedTest
で修飾したメソッドの引数にnull
を渡します。-
プリミティブ型の引数には使用できません
-
-
@EmptySource
:@ParameterizedTest
で修飾したメソッドの引数に 空値 を渡します。対応している引数の型は参照型(java.lang.String, java.util.List, java.util.Set, java.util.Map
)と配列型(int[]
,char[][]
など)です。-
これらのデータ型から派生した型には未対応です。
-
-
@NullAndEmptySource
:@NullSource
と@EmptySource
の 合成アノテーション で、両方の使い方ができます。
パラメタライズテストへ 空白 に相当するさまざま文字列を渡したいときは、@ValueSource を使うといいでしょう。
具体的には @ValueSource(strings = {" ", " ", "\t", "\n"})
のように記述できます。
次のコード例のように、@NullSource
と @EmptySource
に @ValueSource
を組み合わせると、null
や 空値 や 空白 を幅広く入力させることができます。
@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
合成アノテーションの @NullAndEmptySource
を使うと少し短くなります。
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
どちらの場合でも nullEmptyAndBlankStrings(String) メソッドは6回実行されます。
null で1回、空値で1回、@ValueSource の提供する空白文字で4回です。
|
@EnumSource
@EnumSource
を使うと列挙型の定数が使いやすくなります。
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
assertNotNull(unit);
}
value
属性は任意です。
未指定なら先頭のメソッド引数の型を使用します。
先頭のメソッド引数の型が列挙型でなければテストは失敗します。
前のコード例では、先頭のメソッド引数の型 TemporalUnit
は列挙型ではありません(ChronoUnit
の実装するインターフェイスです)。
ですので、@EnumSource
の value
属性に列挙型を指定しなければなりません。
引数の型を ChronoUnit
へ変更すれば、value
属性の指定は不要になります。
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
assertNotNull(unit);
}
names
属性には使用する定数名を指定できます。
未指定なら全ての定数を使用します。
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}
mode
属性を指定するとテストメソッドに渡す定数を細かく制御できます。
次のコード例では指定した名前に一致する定数を除外したり、指定した正規表現がマッチする定数を選択したりします。
@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) {
assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}
@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
assertTrue(unit.name().endsWith("DAYS"));
}
@MethodSource
@MethodSource
では、テストクラスやそれ以外のクラスに定義した、1つ以上の ファクトリ メソッドを指定できます。
テストクラスを @TestInstance(Lifecycle.PER_CLASS)
で修飾している場合を除いて、テストクラス中のファクトリメソッドは static
宣言したものにしなければいけません。
一方、外部のクラスに定義したファクトリメソッドは必ず static
宣言したものにしなければいけません。
あと、ファクトリメソッドに指定するメソッドは引数を宣言できません。
ファクトリメソッドは 引数に渡す値 の ストリーム を生成しなければなりません。
ストリームのそれぞれの要素は @ParameterizedTest
で修飾したメソッドを呼び出すたびに渡されます。
一般的には Arguments
の Stream
つまり Stream<Arguments>
という形式で表現する場合が多いようです。
ただし、ファクトリメソッドの返り値型は他にもいろいろな型になります。
JUnit は安全に Stream
へ変換できる型を「ストリーム」と呼びます(それぞれのプリミティブ型に対応する Stream
や、オブジェクト型の配列、プリミティブ型の配列、Collection
や Iterator
や Iterable
など)。
そして、Arguments
のインスタンスとして渡せるものならなんでも「ストリームの要素」になります(オブジェクト型の配列や、単一のリテラル値など)。
テストメソッドの引数が1つだけなら、引数と同じ型の Stream
を返すファクトリメソッドを使用できます。
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
@MethodSource
で明示的にメソッド名を指定したくないときのために、JUnit Jupiter では @ParameterizedTest
で修飾したメソッド名と同じ名前のメソッドを探索して、 ファクトリメソッド として使うようになっています。
@ParameterizedTest
@MethodSource
void testWithDefaultLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> testWithDefaultLocalMethodSource() {
return Stream.of("apple", "banana");
}
ファクトリメソッドの返り値型には、プリミティブ型に対応する Stream
を使用できます。
@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
assertNotEquals(9, argument);
}
static IntStream range() {
return IntStream.range(0, 20).skip(10);
}
パラメタライズテストに複数の引数を宣言した場合、ファクトリメソッドの返り値型は Arguments
を要素型とするコレクション、ストリーム、配列にするか、オブジェクト型の配列にしなければなりません(対応している返り値型について詳しくは @MethodSource
の Javadoc を参照してください)。
arguments(Object…)
は Arguments
インターフェイスに定義した static ファクトリメソッドです。
代わりに Arguments.of(Object…)
を使うこともできます。
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
外部のクラスで static
宣言した ファクトリメソッド は 完全修飾メソッド名 で指定できます。
package example;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
class ExternalMethodSourceDemo {
@ParameterizedTest
@MethodSource("example.StringsProviders#tinyStrings")
void testWithExternalMethodSource(String tinyString) {
// test with tiny string
}
}
class StringsProviders {
static Stream<String> tinyStrings() {
return Stream.of(".", "oo", "OOO");
}
}
@CsvSource
@CsvSource
を使うと、引数のリストをカンマ区切りで表現できます(例えば文字列リテラル)。
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
既定の区切り文字はカンマ(,
)ですが、delimiter
属性で別の文字を指定できます。
delimiterString
属性には区切り文字ではなく区切り文字列を指定できます。
両方の属性を同時に設定することはできません。
既定の囲み文字は一重引用符('
)です。
この次の具体例にあるとおり、'lemon, lime'
のように記述できます。
emptyValue
属性が未指定なら ''
は空文字列になります。
囲み文字すら無い完全な 空値 は null
になります。
nullValues
属性に指定した1つ以上の文字列のいずれかに対応する値は null
になります(この次の具体例で NIL
の例を参照してください)。
プリミティブ型のメソッド引数に null
を渡すと ArgumentConversionException
をスローします。
nullValues 属性にどんな値を設定しても、囲み文字で囲んでない 空値は常に null になります。
|
既定では、囲み文字があってもなくても、CSV の列の値の前後の空白は除去します。
ignoreLeadingAndTrailingWhitespace
属性に false
を設定すると、空白を除去しなくなります(JUnit Jupiter 5.8 で導入された機能です)。
サンプルコード | 引数に渡される値のリスト |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
@CsvFileSource
@CsvFileSource
を使うとクラスパスやローカルファイルシステムに配置した CSV ファイルを使用できます。
CSV ファイルの各行ごとに、パラメタライズテストのテストメソッドを実行します。
既定の区切り文字はカンマ(,
)ですが、delimiter
属性で別の文字を指定できます。
delimiterString
属性には区切り文字ではなく区切り文字列を指定できます。
両方の属性を同時に設定することはできません。
CSVファイル中のコメント行の扱い
\# で始まる行はコメントと見做して、すべて無視します。
|
@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromClasspath(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
@ParameterizedTest
@CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
Country, reference
Sweden, 1
Poland, 2
"United States of America", 3
France, 700_000
@CsvSource
と違って、@CsvFileSource
では囲み文字として二重引用符("
)を使います。
前の例では "United States of America"
のように使っています。
emptyValue
属性が未指定なら ""
は空文字列になります。
囲み文字すら無い完全な 空値 は null
になります。
nullValues
属性に指定した1つ以上の文字列のいずれかに対応する値は null
になります。
プリミティブ型のメソッド引数に null
を渡すと ArgumentConversionException
をスローします。
nullValues 属性にどんな値を設定しても、囲み文字で囲んでない 空値は常に null になります。
|
既定では、囲み文字があってもなくても、CSV の列の値の前後の空白は除去します。
ignoreLeadingAndTrailingWhitespace
属性に false
を設定すると、空白を除去しなくなります(JUnit Jupiter 5.8 で導入された機能です)。
@ArgumentsSource
@ArgumentsSource
は自作の再利用可能な ArgumentsProvider
を指定するために使います。
ArgumentsProvider
の実装クラスはトップレベルクラスか static
宣言した内部クラスにしなければいけません。
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana").map(Arguments::of);
}
}
2.15.4. 引数の変換
ワイドニング変換
JUnit Jupiter はパラメタライズテストに渡される引数の プリミティブ型のワイドニング変換 に対応しています。
例えば、テストメソッドを @ValueSource(ints = { 1, 2, 3 })
で修飾した場合、メソッド引数の型には int, long, float, double
が使用できます。
暗黙的な型変換
JUnit Jupiter は @CsvSource
に必要な暗黙の型変換処理をいくつも提供しています。
メソッド引数の型によって、異なる変換処理を実行するということです。
例えば、パラメタライズテストのメソッド引数の型が TimeUnit
で、ソースのデータ型が String
なら、TimeUnit
列挙型の対応する定数へ自動的に変換します。
@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(ChronoUnit argument) {
assertNotNull(argument.name());
}
String
からは、次のようなメソッド引数の型へ暗黙的に型変換します。
String リテラルで10進数記法、16進数記法、8進数記法の数値を指定した場合、byte, short, int, long の対応するプリミティブ型か、プリミティブ型に対応するラッパークラスへ変換します。
|
Target Type | Example |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
文字列からオブジェクトへ変換するときの代替処理
JUnit Jupiter は文字列からメソッド引数の型への型変換(前のセクションで表に列挙した)だけでなく、文字列からの変換処理が未定義の場合の代替処理として、自動的に単一の ファクトリメソッド あるいは、単一の ファクトリコンストラクタ を探索し、実行できます。
-
ファクトリメソッド: 可視性は
package
以上。static
宣言している。引数はString
1つ。返り値は対象データ型のインスタンスである、対象データ型に定義したメソッド。メソッド名は自由です。 -
ファクトリコンストラクタ: 可視性は
package
以上。引数はString
1 つ。対象データ型はトップレベルクラスかstatic
宣言したインナークラスにしなければいけません。
ファクトリメソッド を複数発見したときは何も見つからなかったものとします。ファクトリメソッド と ファクトリコンストラクタ が見つかったときはファクトリメソッドを優先して使用します。 |
次のコード例は、ファクトリメソッド Book.fromTitle(String)
で "42 Cats"
から作成した Book
のインスタンスをテストメソッドに渡しています。
@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
assertEquals("42 Cats", book.getTitle());
}
public class Book {
private final String title;
private Book(String title) {
this.title = title;
}
public static Book fromTitle(String title) {
return new Book(title);
}
public String getTitle() {
return this.title;
}
}
明示的な型変換
暗黙的な型変換に頼れない場合は、メソッド引数を @ConvertWith
で修飾し、ArgumentConverter
を指定することでデータ型を変換します。
ArgumentConverter
の実装クラスはトップレベルクラスか static
宣言したインナークラスにしなければいけません。
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithExplicitArgumentConversion(
@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(ChronoUnit.valueOf(argument));
}
public class ToStringArgumentConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
assertEquals(String.class, targetType, "Can only convert to String");
if (source instanceof Enum<?>) {
return ((Enum<?>) source).name();
}
return String.valueOf(source);
}
}
データ型を変換するだけなら、TypedArgumentConverter
を継承すれば、定型的な型チェックを省略できます。
public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {
protected ToLengthArgumentConverter() {
super(String.class, Integer.class);
}
@Override
protected Integer convert(String source) {
return (source != null ? source.length() : 0);
}
}
引数の明示的な型変換を実装するのは、テストや拡張機能の作者の責任です。
それゆえに、junit-jupiter-params
では参照実装として JavaTimeArgumentConverter
だけを提供しています。
合成アノテーションの JavaTimeConversionPattern
と一緒に使います。
@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(
@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
assertEquals(2017, argument.getYear());
}
2.15.5. 引数の集約
既定では、パラメタライズテストに渡すそれぞれの 引数 は、1つ1つのメソッド引数に対応します。 つまり、複数の引数をかたまりとして提供するソースには、それだけ多くのメソッド引数が必要になるのです。
そういう場合はメソッド引数を複数宣言する代わりに ArgumentsAccessor
を使用します。
この API を使用すると、複数の引数を単一のメソッド引数に渡すことができるようになります。
また、データ型の変換は 暗黙的な型変換 と同様に行われます。
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
Person person = new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, Gender.class),
arguments.get(3, LocalDate.class));
if (person.getFirstName().equals("Jane")) {
assertEquals(Gender.F, person.getGender());
}
else {
assertEquals(Gender.M, person.getGender());
}
assertEquals("Doe", person.getLastName());
assertEquals(1990, person.getDateOfBirth().getYear());
}
_ ArgumentsAccessor
型の引数には自動的に ArgumentsAccessor
のインスタンスが注入されます。_
独自の集約
パラメタライズテストの中で直接 ArgumentsAccessor
から引数へアクセスする代わりに、独自の再利用可能な アグリゲーター を使用できるようになっています。
独自のアグリゲーターをパラメタライズテストで使用するには、互換性のあるデータ型のメソッド引数を @AggregateWith
で修飾し、ArgumentsAggregator
インターフェイスの実装クラスを指定します。
テストメソッドを実行するとき、メソッド引数には集約した結果が渡されます。
ArgumentsAggregator
の実装クラスはトップレベルクラスか static
宣言したインナークラスにしなければいけません。
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
// perform assertions against person
}
public class PersonAggregator implements ArgumentsAggregator {
@Override
public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) {
return new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, Gender.class),
arguments.get(3, LocalDate.class));
}
}
コードベースのあちこちで、テストメソッドの複数の引数を @AggregateWith(MyTypeAggregator.class)
で修飾しているような場合、@AggregateWith(MyTypeAggregator.class)
のメタアノテーションとなる @CsvToMyType
のような 合成アノテーション が欲しくなるでしょう。
次のコード例は PersonAggregator
を使用する @CsvToPerson
アノテーションを定義しています。
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
// perform assertions against person
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}
2.15.6. 表示名の変更
パラメタライズテストの表示名には、呼び出し回数を表す数値と、全てのメソッド引数の文字列表現が含まれています。
ArgumentsAccessor
や ArgumentAggregator
以外のメソッド引数の値の前には、バイトコードに含まれている引数名が指定されています(テストコードをコンパイルするときに -parameters
フラグを指定した場合です)。
しかし、@ParameterizedTest
の name
属性に指定した文字列パターンで表示名は変更できます。
@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
@CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
void testWithCustomDisplayNames(String fruit, int rank) {
}
前のコード例を ConsoleLauncher
で実行すると、次のように出力されます。
Display name of container ✔ ├─ 1 ==> the rank of 'apple' is 1 ✔ ├─ 2 ==> the rank of 'banana' is 2 ✔ └─ 3 ==> the rank of 'lemon, lime' is 3 ✔
name
属性に指定できるのは MessageFormat
が対応している文字列パターンです。
一重引用符('
)を使いたいときは2つ続けて(''
)のようにしなければなりません。
表示名の文字列パターンが対応しているプレースホルダは次のとおりです。
プレースホルダ | 内容 |
---|---|
|
メソッド名 |
|
1から数え始める呼び出し回数 |
|
カンマ区切りにした引数の値 |
|
カンマ区切りにした引数の名前と値 |
|
位置に対応する引数の値 |
引数を表示名に含めるとき、最大文字列長を超過するなら文字列表現は短縮されます。最大文字列長は junit.jupiter.params.displayname.argument.maxlength で指定できます。初期値は 512 文字です。
|
@MethodSource
や @ArgumentSource
を使うときは引数に名前を指定できます。
表示名の文字列パターンに引数の名前が含まれているときは、指定した名前が使われます。
@DisplayName("A parameterized test with named arguments")
@ParameterizedTest(name = "{index}: {0}")
@MethodSource("namedArguments")
void testWithNamedArguments(File file) {
}
static Stream<Arguments> namedArguments() {
return Stream.of(arguments(Named.of("An important file", new File("path1"))),
arguments(Named.of("Another file", new File("path2"))));
}
A parameterized test with named arguments ✔ ├─ 1: An important file ✔ └─ 2: Another file ✔
プロジェクトに含まれる全てのパラメタライズテストで使用する表示名の文字列パターンを指定するには、junit-platform.properties
を次のように記述します。
junit.jupiter.params.displayname.default = {index}
パラメタライズテストの表示名を決定する規則は次のとおりです。
-
@ParameterizedTest
のname
属性の値 -
(存在するなら)
junit-platform.properties
で指定されたjunit.jupiter.params.displayname.default
の値 -
@ParameterizedTest
に定義された定数DEFAULT_DISPLAY_NAME
の値
2.15.7. ライフサイクルと相互運用性
パラメタライズテストのそれぞれの呼び出しには、普通の @Test
メソッドを呼び出すのと同じライフサイクルが伴います。
例えば、@BeforeEach
は毎回呼び出されるということです。
実行時にテストを生成する と同様に、IDE からはそれぞれの呼び出し結果が1つのツリーにぶら下がるように見えることになります。
同じテストクラスで @ParameterizedTest
と普通の @Test
を混ぜて使用できます。
@ParameterizedTest
と ParameterResolver
拡張を組み合わせて使用するとしても、メソッド引数の前のほうには @ArgumentSource
で解決する引数を定義しなければなりません。
テストクラスにパラメタライズテストと普通のテストメソッドを定義している、かつ、普通のテストメソッドにはパラメタライズテストと異なる引数を定義しているとしたら、普通のテストメソッドを実行するとき、@BeforeEach
などのライフサイクルメソッドやテストクラスのコンストラクタでは、@ArgumentSource
による引数の解決は行われません。
@BeforeEach
void beforeEach(TestInfo testInfo) {
// ...
}
@ParameterizedTest
@ValueSource(strings = "apple")
void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
testReporter.publishEntry("argument", argument);
}
@AfterEach
void afterEach(TestInfo testInfo) {
// ...
}
2.16. テストのテンプレート化
@TestTemplate
で修飾したメソッドは、普通のテストメソッドではなくテンプレートとして使われます。
登録したプロバイダーの返す呼び出しコンテキストの数と同じだけ呼び出す前提で設計します。
つまり、定義済みの TestTemplateInvocationContextProvider
拡張と組み合わせて使用しなければなりません。
それぞれの呼び出しは、普通の @Test
メソッドと完全に同じライフサイクルメソッドと拡張機能に対応しています。
具体的な使い方は テストテンプレートに呼び出し元のコンテキストを与える を参照してください。
繰り返しテスト と パラメタライズテスト はテストのテンプレート化機能で実装されています。 |
2.17. 実行時にテストを生成する
JUnit Jupiter における基本的な @Test
アノテーション(アノテーション を参照)は、JUnit 4 の @Test
と非常によく似た機能を提供します。
あるメソッドがテストを実装したものであることを示しているのです。
これらのテストはコンパイル時に確定するため、ある意味静的なものであり、実行時に振る舞いを変更できません。
仮定(Assumption)は動的な振る舞いの基本的な形態ではありますが、その表現力は意図的に制限されています。
JUnit Jupiter では、これまでの標準的なテストとは全く異なる新しいテストプログラミングモデルとして 動的テスト を導入しました。
@TestFactory
で修飾したファクトリメソッドで、実行時にテストメソッドを生成するのです。
普通のテストメソッドと違って、@TestFactory
で修飾したメソッドはテストメソッドではなく、ファクトリメソッドなのです。
ファクトリメソッドで、動的テストを生成するのです。
具体的には、@TestFactory
で修飾したファクトリメソッドの返り値は、単独の DynamicNode
か、DynamicNode
を要素とする Stream, Collection, Iterable, Iterator
あるいは配列にします。
DynamicNode
の派生クラス DynamicContainer
および DynamicTest
はインスタンス化可能です。
DynamicContainer
は 表示名 と DynamicNode
のリストを組み合わせたもので、任意の入れ子構造を作成できます。
DynamicTest
は遅延評価(実行)するインスタンスで、実行時に、非決定論的なテストを表現できます。
@TestFactory
メソッドの返り値が Stream
の場合、Files.lines()
のようにリソースを安全に使うため、stream.close()
で閉じないといけいません。
@Test
メソッドと同じく、@TestFactory
メソッドの可視性は private
にできません。
また、static
宣言することもできません。
さらに、ParameterResolvers
でメソッド引数を解決できます。
DynamicTest
は実行時に生成するテストケースで、表示名 と Executable
を組み合わせたものです。
Executable
は @FunctionalInterface
の実装クラスのことで、動的クラスにおいては ラムダ式 や メソッド参照 として記述できます。
動的テストのライフサイクル
動的テストのライフサイクルは普通のテストメソッドとは大きく異なります。特に違うのは、動的テストにはライフサイクルメソッドの呼び出しが伴わないことです。
つまり、@TestFactory メソッドを呼び出すときは @BeforeEach や @AfterEach メソッド(および関連する拡張機能)も呼び出されるのですが、動的テスト自体を実行するときは呼び出されないのです。
言い換えると、動的テストの実装であるラムダ式からテストクラスのインスタンスフィールドへアクセスするとしても、同じ @TestFactory メソッドの生成したそれぞれの動的テストを呼び出している間に、フィールドを初期化するライフサイクルメソッドは呼び出されないのです。
|
JUnit Jupiter 5.8.0 の時点では、ファクトリメソッドでしか動的テストを生成できませんが、将来的には登録方式でも生成できるようになるでしょう。
2.17.1. 動的テストの具体例
次のコード例では DynamicTestsDemo
クラスにより動的テストのいろいろな使い方を紹介します。
最初のメソッドは返り値の型が間違っているのですが、コンパイル時に検出できないので、実行時に JUnitException
をスローします。
2つ目から続く6つのメソッドは、DynamicTest
を要素とする Collection, Iterable, Iterator, 配列, Stream
を返り値の型にしています。
特に動的な振る舞いを説明するものではありませんが、対応している返り値の型は分かると思います。
dynamicTestsFromStream()
や dynamicTestsFromIntStream()
を見れば、複数の文字列や、数値の範囲に基づく動的なテストを簡単に生成できるのが分かります。
generateRandomNumberOfTests()
は動的テストの本来の性質を活用しています。
乱数を生成する Iterator
、表示名を生成するラムダ式、テストロジックを実装したラムダ式を DynamicTest.stream()
へ渡しています。
どの辺が本来の性質なのかというと、実行するたびに結果が変化するところです。
動的テストの表現力を証明するためのコード例なので、実際に使用する場合は注意してください。
generateRandomNumberOfTests()
と同じように柔軟性を活用しているのが dynamicTestsFromStreamFactoryMethod()
です。
ファクトリメソッドの DynamicTest.stream()
へ Stream
を渡しているのが違うところです。
dynamicNodeSingleTest()
が単一の DynamicTest
を返すようになっているのはあくまでも説明用です。
dynamicNodeSingleContainer()
では、DynamicContainer
で動的テストの入れ子構造を作成しています。
import static example.util.StringUtils.isPalindrome;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.junit.jupiter.api.Named.named;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import example.util.Calculator;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
class DynamicTestsDemo {
private final Calculator calculator = new Calculator();
// This will result in a JUnitException!
@TestFactory
List<String> dynamicTestsWithInvalidReturnType() {
return Arrays.asList("Hello");
}
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(
dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(
dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
).iterator();
}
@TestFactory
DynamicTest[] dynamicTestsFromArray() {
return new DynamicTest[] {
dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
};
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
// Generates tests for the first 10 even integers.
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
}
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {
// Generates random positive integers between 0 and 100 until
// a number evenly divisible by 7 is encountered.
Iterator<Integer> inputGenerator = new Iterator<Integer>() {
Random random = new Random();
int current;
@Override
public boolean hasNext() {
current = random.nextInt(100);
return current % 7 != 0;
}
@Override
public Integer next() {
return current;
}
};
// Generates display names like: input:5, input:37, input:85, etc.
Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
// Executes tests based on the current input value.
ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
// Stream of palindromes to check
Stream<String> inputStream = Stream.of("racecar", "radar", "mom", "dad");
// Generates display names like: racecar is a palindrome
Function<String, String> displayNameGenerator = text -> text + " is a palindrome";
// Executes tests based on the current input value.
ThrowingConsumer<String> testExecutor = text -> assertTrue(isPalindrome(text));
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
// Stream of palindromes to check
Stream<Named<String>> inputStream = Stream.of(
named("racecar is a palindrome", "racecar"),
named("radar is also a palindrome", "radar"),
named("mom also seems to be a palindrome", "mom"),
named("dad is yet another palindrome", "dad")
);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream,
text -> assertTrue(isPalindrome(text)));
}
@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
return Stream.of("A", "B", "C")
.map(input -> dynamicContainer("Container " + input, Stream.of(
dynamicTest("not null", () -> assertNotNull(input)),
dynamicContainer("properties", Stream.of(
dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
))
)));
}
@TestFactory
DynamicNode dynamicNodeSingleTest() {
return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop")));
}
@TestFactory
DynamicNode dynamicNodeSingleContainer() {
return dynamicContainer("palindromes",
Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
));
}
}
2.17.2. 動的テストのための URI テストソース
JUnit Platform の提供する TestSource
は、テストやテストコンテナを表現するインターフェイスです。
テストメソッドの作成位置や取得元を IDE およびビルドツールに教えるために使用します。
動的テスト(あるいは動的テストコンテナ)の TestSource
を作成するときは、 DynamicTest.dynamicTest(String, URI, Executable)
や DynamicTest.dynbamicContainer(String, URI, Stream)
のようにファクトリメソッドへ java.net.URI
を渡すようにします。
JUnit Jupiter は URI
を解釈して次のいずれかの実装クラスを生成します。
ClasspathResourceSource
-
URI
のプロトコルスキームがclasspath
のとき。例えばclasspath:/test/foo.xml?line=20,column=2
DirectorySource
-
URI
がファイルシステム上のディレクトリを指しているとき。 FileSource
-
URI
がファイルシステム上のファイルを指しているとき。 MethodSource
-
URI
のプロトコルスキームがmethod
で、位置が完全修飾メソッド名(FQMN:Fully Qualified Method Name)のとき。例えばmethod:org.junit.Foo#bar(Java.lang.String, java.lang.String[])
FQMN の形式についてはDiscoverySelectors.selectMethod(String)
の Javadoc を参照してください。 ClassSource
-
URI
のプロトコルスキームがclass
で、位置が完全修飾クラス名(FQCN:Fully Qualified Class Name)のとき。例えばclass:org.junit.Foo#line=42
UriSource
-
上記のどれにも該当しないとき。
2.18. タイムアウト
テストメソッド、テストファクトリメソッド、テストテンプレート、ライフサイクルメソッドのいずれかを @Timeout
で修飾すると、指定した時間を過ぎても終了しなかったら失敗させることができるようになります。
時間単位の初期値は秒ですが、自由に変更できます。
次のコード例は @Timeout
の使い方を説明しています。
class TimeoutDemo {
@BeforeEach
@Timeout(5)
void setUp() {
// fails if execution time exceeds 5 seconds
}
@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds100Milliseconds() {
// fails if execution time exceeds 100 milliseconds
}
}
assertTimeoutPreemptively()
と違って、修飾したメソッドの実行時間は、テストの主スレッドが計測します。
メソッドの実行時間が指定した時間を超過したら、他のスレッドから主スレッドに対して割り込みを発生させます。
どうしてこういう仕組みにしているのかというと、Spring Framework のように、テストメソッドを実行しているスレッドの ThreadLocal
でトランザクション状態を管理している場合があるからです。
トップレベルクラスを @Timeout
で修飾すれば、(@Nested
クラスで定義したメソッドも含めて)全てのテストメソッド、テストファクトリ、テストテンプレートに同じ時間制限を適用できます。
それぞれのメソッド(あるいは @Nested
クラス)で時間制限を上書きするには @Timeout
で修飾します。
クラスを修飾した場合、ライフサイクルメソッドに時間制限は適用されない点に注意してください。
テストファクトリを @Timeout
で修飾しても、テストファクトリを実行する時間、つまり、動的テストを生成する時間だけがチェックされます。
それぞれの動的テストで実行時間を制限するには assertTimeout()
や assertTimeoutPreemptively()
を使用してください。
@RepeatedTest
や @ParameterizedTest
などのテストテンプレートについては、それぞれのテストメソッドについて @Timeout
で指定した時間制限が適用されます。
次の 設定パラメータ を設定すると、テストメソッドやライフサイクルメソッドの種類に応じて、@Timeout
を指定しなかったメソッドのタイムアウト時間を変更できます。
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
で修飾したメソッドのタイムアウト時間。
より抽象度の低い設定項目は、抽象度の高い設定項目の値を上書きします。
具体的には、junit.jupiter.execution.timeout.test.method.default
は junit.jupiter.execution.timeout.testable.method.default
を上書きし、junit.jupiter.execution.timeout.testable.method.default
は junit.jupiter.execution.timeout.default
を上書きします。
設定パラメータに記述する値の形式は <number> [ns|μs|ms|s|m|h|d]
です(大文字小文字は区別しません)。
数値と時間単位の間の空白は無視します。
時間単位を指定しなかった場合は秒として解釈します。
設定値 | アノテーションで同じ結果になる記述 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.18.1. ポーリングテストで @Timeout を使用する
非同期処理を実行するコードでは、結果に対するアサーションを実行する前に、定期的に結果を確認するようなテストを書く場合があります。
そういうロジックは CountDownLatch
のような同期のための仕組みで実装できる場合もありますが、できない場合が問題です。
例えば、テスト対象のコードが外部のメッセージブローカーのチャンネルにメッセージを送信する場合、送信が成功(完了)するまでアサーションを実行できません。
非同期処理をテストするコードでは、テストスイート全体がハングアップしないようタイムアウト時間を設定しなければなりません。
非同期処理のテストで定期的に結果を確認するためにタイムアウト時間を設定すれば、テストが永遠に止まらないことを避けられます。
次のコード例のように、@Timeout
で「指定した時間まで待ち続ける」ロジックを簡単に記述できます。
@Test
@Timeout(5) // Poll at most 5 seconds
void pollUntil() throws InterruptedException {
while (asynchronousResultNotAvailable()) {
Thread.sleep(250); // custom poll interval
}
// Obtain the asynchronous result and perform assertions
}
非同期処理のテストにおいて、定期的な確認時間を指定する以上の柔軟性が必要なときは、外部ライブラリの Awaitility を検討してください。 |
2.19. 並列実行
並列実行機能は実験的な機能です
JUnit チームがこの機能を改善して、正式機能へ 昇格 できるよう、試用してフィードバックを提供してください。
|
JUnit Jupiter はテストを1スレッドで逐次的に実行します。
5.3 から導入された並列実行機能は、テスト実行時間を短縮するための機能で、使用するには明示的な設定が必要でした。
設定パラメータの junit.jupiter.execution.parallel.enabled
を true
指定しなければなりません。
設定する場所は Junit-platform.properties
です(他の設定項目については 設定パラメータの構成 を参照してください)。
この設定項目を有効化するのは、テストを並列実行するための最初の一歩にすぎません。 設定項目を有効化しただけではテストの実行方法は変わりません。 テストツリーを構成するどのノードを並行して実行するかは、実行モードによって異なります。 実行モードに指定できる値は次の2つです。
SAME_THREAD
-
強制的に親ノードと同じスレッドで実行します。 例えば、テストメソッドに指定した場合、同じテストクラスの全ての
@BeforeAll
と@AfterAll
は同じスレッドで実行します。 CONCURRENT
-
リソースロックにより同じスレッドでの実行を強制されない限り、並行して実行します。
基本的にテストツリーのノードの実行モードは SAME_THREAD
になっています。
これは設定パラメータの junit.jupiter.execution.parallel.mode.default
で変更できます。
実行モードは @Execution
アノテーションでも変更できます。
このアノテーションで修飾した要素やその子要素は、それぞれのテストクラスの中で並列に実行するようになります。
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
一部の例外を除いて、既定の実行モードは全てのノードに適用されます。
例外は、インスタンスライフサイクル管理がクラス単位(per-class)になっているテストクラスと、@TestMethodOrder
で MethodOrderer.Random` 以外の {MethodOrder}
を指定しているテストクラスです。
前者の場合、テストの作成者はテストクラスがスレッド安全であることを保証しなければなりません。
後者の場合、並行実行と実行順序の指定が矛盾してしまいます。
それゆえに、どちらの場合もテストクラスやテストメソッドが @Execution(CONCURRENT)
で修飾されている場合だけ、並行して実行するようになっています。
宣言的な 同期化機構 が使われているとしても、テストツリーの全てのノードの実行モードが CONCURRENT
になっているなら、設定パラメータ に応じて全てを並行して実行します。
標準出力や標準エラー出力をキャプチャーする は独立して有効化しなければなりません。
さらに、トップレベルクラスの既定の実行モードを junit.jupiter.execution.parallel.mode.classes.default
で変更できます。
両方の設定項目を組み合わせれば、テストクラスは並列に実行するけど、テストメソッドは同じスレッドで実行させることができます。
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
逆に、同じテストクラスのテストメソッドは並列に実行するけど、トップレベルクラスは逐次的に実行させることもできます。
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread
次の図は、設定パラメータ junit.jupiter.execution.parallel.mode.default
と junit.jupiter.execution.parallel.mode.classes.default
の値の全ての(4種類の)組み合わせについて、2つのトップレベルクラス A
と B
それぞれに定義した2つのテストメソッドがどのように実行させるかを示しています。
設定パラメータ junit.jupiter.execution.parallel.mode.classes.default
を指定しなかったときは、代わりに junit.jupiter.execution.parallel.mode.default
の設定値を使用します。
2.19.1. 設定方法
並列度とスレッドプールサイズの最大値は ParallelExecutionConfigurationStrategy
で設定します。
JUnit Platform は dynamic
と fixed
という2種類の実装を提供しています。
自分で作成する場合は custom
を指定します。
To select a strategy, set the junit.jupiter.execution.parallel.config.strategy
configuration parameter to one of the following options.
設定パラメータ junit.jupiter.execution.parallel.config.strategy
に指定できるのは次の値です。
dynamic
-
利用できるCPU数(CPUコア数)に、設定パラメータ
junit.jupiter.execution.parallel.config.dynamic.factor
の値(初期値は1
)を乗じた値を並列度とします。 fixed
-
設定パラメータ
junit.jupiter.execution.parallel.config.fixed.parallelism
で並列度を指定します。 custom
-
ParallelExecutionConfigurationStrategy
の実装クラス(の完全修飾クラス名)を設定パラメータのjunit.jupiter.execution.parallel.config.custom.class
に指定します。
未設定の場合、係数 1
の dynamic
が指定されたものとします。
つまり、並列度はCPU数(CPUコア数)と等しくなります。
並列度は並行動作するスレッド数の最大値そのものではありません
並行して実行するテストの数(スレッド数)は、並列度の設定値を超過する場合があります(JUnit Jupiter は設定値を超過しないことを保証しません)。
例えば、次のセクションで説明している同期化機構 ForkJoinPool では、設定した並列度を満たすだけのテストを実行し続けるために、並列度を越える数のスレッドを生成する場合があります。
したがって、テストを実行するリソースをきちんと制御したければ、並列度を制御するロジックを自分で作成しなければなりません。
|
2.19.2. 同期化
JUnit Jupiter は、実行モードを制御する @Execution
アノテーションに加えて、宣言的に同期させるための @ResourceLock
アノテーションを提供しています。
テストクラスやテストメソッドなど、同期アクセスの必要な共有リソースを @ResourceLock
で修飾することで、安全に実行できることを保証します。
共有リソースを区別する名前は文字列で指定します。
独自の文字列を指定するか、Resources
の定義済み定数 SYSTEM_PROPERTIES, SYSTEM_OUT, SYSTEM_ERR, LOCALE, TIME_ZONE
を使用できます。
次のコード例に登場するテストを @ResourceLock
を 使わないで 並列に実行すると、実行結果が フレーキー(flaky) になってしまいます。
JVM の同じシステムプロパティの読み書きには潜在的に競合状態があるため、ある時は成功し、ある時は失敗するようになってしまうのです。
共有リソースにアクセスするテストクラスやテストメソッドは @ResourceLock
で修飾します。
JUnit Jupiter はこのアノテーションを参照して、並列に実行するテストが衝突しないことを保証します。
テストを独立して実行する
ほとんどのテストクラスは同期化せずに実行できるけど、一部のテストクラスは独立して実行させたい場合、後者のテストクラスを |
文字列で指定した名前だけでなく、アクセスモードを指定して共有リソースを識別できます。
共有リソースに読み取りアクセス(READ
)をする2つのテストは並列に実行できるけど、読み取りアクセスと書き込みアクセスの両方(READ_WRITE
)が必要な2つのテストは並列に実行できません。
@Execution(CONCURRENT)
class SharedResourcesDemo {
private Properties backup;
@BeforeEach
void backup() {
backup = new Properties();
backup.putAll(System.getProperties());
}
@AfterEach
void restore() {
System.setProperties(backup);
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ)
void customPropertyIsNotSetByDefault() {
assertNull(System.getProperty("my.prop"));
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
void canSetCustomPropertyToApple() {
System.setProperty("my.prop", "apple");
assertEquals("apple", System.getProperty("my.prop"));
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
void canSetCustomPropertyToBanana() {
System.setProperty("my.prop", "banana");
assertEquals("banana", System.getProperty("my.prop"));
}
}
2.20. 組み込みの拡張機能
JUnit チームとしては、再利用可能な拡張機能を別のライブラリで提供することを推奨しています。 ですが、JUnit Jupiter API のアーティファクトには、ユーザーが頻繁に使用するであろう便利な拡張機能を同梱しています(依存ライブラリを追加しなくても使用できます)。
2.20.1. 一時ディレクトリ
一時ディレクトリ
JUnit チームがこの機能を改善して、正式機能へ 昇格 できるよう、試用してフィードバックを提供してください。
@TempDir は実験的な機能です |
TempDirectory
拡張機能は、一時的にテストが使用するディレクトリの作成と後始末をするための拡張機能です。
テストメソッド単位や、テストクラス単位で使用できます。
始めから登録されている拡張機能です。
java.nio.file.Path
や java.io.File
のフィールドを @TempDir
で修飾して使用します。
また、テストメソッドやライフサイクルメソッドについて、java.nio.file.Path
や java.io.File
の引数を @TempDir
で修飾して使用します。
次のコード例では、テストメソッドの引数を @TempDir
で修飾しています。
作成された一時ディレクトリにファイルを作成し、その内容を確認しているのが分かります。
@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}
複数の引数を修飾すれば、複数の一時ディレクトリを作成し、注入できます。
@Test
void copyFileFromSourceToTarget(@TempDir Path source, @TempDir Path target) throws IOException {
Path sourceFile = source.resolve("test.txt");
new ListWriter(sourceFile).write("a", "b", "c");
Path targetFile = Files.copy(sourceFile, target.resolve("test.txt"));
assertNotEquals(sourceFile, targetFile);
assertEquals(singletonList("a,b,c"), Files.readAllLines(targetFile));
}
テストクラス全体あるいはテストメソッド(アノテーションで修飾する場所によって異なります)に対して単一の一時ディレクトリを提供する、という昔の振る舞いに戻すときは、設定パラメータ junit.jupiter.tempdir.scope に per_context を設定します。
この設定パラメータは将来のリリースで廃止予定です。
|
コンストラクタ引数では @TempDir
を使用できません。
ライフサイクルメソッドと特定のテストメソッドで同じ一時ディレクトリを使用するときは、テストクラスのインスタンスフィールドを @TempDir
で修飾してください。
次のコード例では static
フィールドの一時ディレクトリを 共有 ディレクトリとして使うようになっています。
こうすると、テストクラスの全てのライフサイクルメソッドとテストメソッドは同じ sharedTempDir
へアクセスできます。
インスタンスフィールドを使えば、それぞれテストメソッドが別のディレクトリを使うようにできるので、隔離性は高まります。
class SharedTempDirectoryDemo {
@TempDir
static Path sharedTempDir;
@Test
void writeItemsToFile() throws IOException {
Path file = sharedTempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}
@Test
void anotherTestThatUsesTheSameTempDir() {
// use sharedTempDir
}
}
3. JUnit 4 から移行する
JUnit Jupiter のプログラミングモデルや拡張モデルは、直接的に JUnit 4 の Rules
や Runners
へ対応していません。
しかし、JUnit Jupiter へ移行するために既存のソースコードやテスト用の拡張機能や、独自のテスト基盤を全て書き換えてくれるとは考えられません。
そこで、新しい JUnit Platform で JUnit 3 と JUnit 4 のテストコードを実行できる、JUnit Vintage テストエンジン という安全な移行パスを提供することにしました。
JUnit Jupiter 専用のクラスとアノテーションが所属するのは org.junit.jupiter
パッケージなので、JUnit 4 が同じクラスパスに存在していても、衝突することはありません。
既存の JUnit 4 のテストを保守しながら、JUnit Jupiter のテストを安全に追加できるのです。
また、JUnit チームは JUnit 4.x リリースの保守と不具合対応を継続していくつもりなので、開発者の皆さまは自分たちの都合に合わせて、十分な時間をかけて JUnit Jupiter へ移行できます。
3.1. JUnit Platform で JUnit 4 のテストを実行する
実行時クラスパスに junit-vintage-engine
を置いてください。
それだけで、JUnit Platform launcher は自動的に JUnit 3 と JUnit 4 のテストを発見できるようになります。
junit5-samples
リポジトリを見て、Gradle や Maven の設定方法を確認してください。
3.1.1. カテゴリー(Categories)の対応
JUnit Vintage テストエンジン は、@Category
で修飾したテストクラスやテストメソッドについて、指定したカテゴリの完全修飾クラス名を タグ として公開します。
例えば、テストメソッドを @Category(Example.class)
で修飾しているなら、"com.acme.Example"
というタグとして公開します。
JUnit 4 の Categories
ランナーは、JUnit Vintage テストエンジンの公開したタグからテストを選択したり発見したりできます。
そして、このタグの情報はテストを実行する前に参照できるようになっています(テストを実行する を参照)。
3.2. 移行のポイント
JUnit 4 のテストを JUnit Jupiter へ移行するときは、次のような話題に注意してください。
-
アノテーションは
org.junit.jupiter.api
パッケージで探す -
アサーションは
org.junit.jupiter.api.Assertions
クラスで探す -
アサンプションは
org.junit.jupiter.api.Assumptions
クラスで探す-
JUnit Jupiter 5.4 から JUnit 4 の
org.junit.Assume
クラスのアサンプションを使用できるようになりました。 特に、テストを失敗させる代わりに中断させるため、JUnit 4 のAssumptionViolatedException
をスローします。
-
-
@Before
と@After
の代わりに@BeforeEach
と@AfterEach
を使用する -
@BeforeClass
と@AfterClass
の代わりに@BeforeAll
と@AfterAll
を使用する -
@Ignore
の代わりに@Disabled
を使用する。あるいは組み込みの 条件に基づくテスト実行 を使用する-
JUnit 4 の
@Ignore
対応 も参照してください
-
-
@Category
の代わりに@Tag
を使用する -
@RunWith
の代わりに@ExtendWith
を使用する -
@Rule
と@ClassRule
の代わりに@ExtendWith
と@RegisterExtension
を使用する-
JUnit 4 の
Rule
対応(制限付き) も参照してください
-
3.3. JUnit 4 の Rule
対応(制限付き)
前に述べたとおり、JUnit Jupiter は直接的に JUnit 4 の Rule へ対応していません。 JUnit チームも、大企業を含む多数の組織が独自のルールを活用していることは理解しているつもりです。 そこで、そういう組織に段階的な移行パスを提供するため、JUnit 4 Rule の一部をそのまま JUnit Jupiter で再実装することにしました。 アダプターを使用する実装になっているので、JUnit Jupiter の拡張モデルにおける意味論的な互換性は限定的です。 例えば、テストの全体的な実行フローを完全に変更するのは不可能です。
JUnit Jupiter の junit-jupiter-migrationsupport
モジュールが対応している Rule
(と一部の派生クラス)は次のとおりです。
-
org.junit.rules.ExternalResource
(includingorg.junit.rules.TemporaryFolder
) -
org.junit.rules.Verifier
(includingorg.junit.rules.ErrorCollector
) -
org.junit.rules.ExpectedException
JUnit 4 ではメソッドだけでなくフィールドも Rule で修飾できるようになっていました。
クラスレベルの拡張機能を追加することで、そういう Rule
の使い方をしている従来のコードベースを そのまま 、import 文すら変更せずに実行できるようになります。
テストクラスを @EnableRuleMigrationSupport
で修飾すると、制限付きの Rule
対応を実現できます(クラス単位で切り替えます)。
このアノテーションは 合成アノテーション で、全ての移行機能(VerifierSupport, ExternalResourceSupport, ExpectedExceptionSupport
)を有効化します。
代わりにテストクラスを @EnableJunit4MigrationSupport
で修飾すると、Rule
の移行機能を登録するだけでなく JUnit 4 の @Ignore
アノテーションの移行機能も登録します(JUnit 4 の @Ignore
対応 を参照)。
とはいえ、これから JUnit 5 のために拡張機能を作成するなら、JUnit 4 のルールモデルではなく、JUnit Jupiter の拡張モデルを使用してください。
3.4. JUnit 4 の @Ignore
対応
JUnit Jupiter へのスムーズ移行パスを実現するため、junit-jupiter-migrationsupport
モジュールは JUnit 4 の @Ignore
アノテーションに対応する機能を提供しています。
JUnit Jupiter のテストで @Ignore
を使うには、テスト依存ライブラリ に junit-jupiter-migrationsupport
モジュールを追加して、テストクラスを @ExtendWith(IgnoreCondition.class)
で修飾するか、@EnableJUnit4MigrationSupport
で修飾します(後者は JUnit 4 の Rule
対応(制限付き) と同時に IgnoreCondition
を自動的に登録します)。
IgnoreCondition
は @Ignore
で修飾したテストクラスやテストメソッドを無効化する ExecutionCondition
の実装クラスです。
import org.junit.Ignore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.migrationsupport.EnableJUnit4MigrationSupport;
// @ExtendWith(IgnoreCondition.class)
@EnableJUnit4MigrationSupport
class IgnoredTestsDemo {
@Ignore
@Test
void testWillBeIgnored() {
}
@Test
void testWillBeExecuted() {
}
}
4. テストを実行する
4.1. IDE でテストを実行する
4.1.1. IntelliJ IDEA
IntelliJ IDEA は 2016.2 から JUnit Platform でテストを実行できるようになりました。
詳しくは IntelliJ IDEA ブログの投稿 を参照してください。
ただし、実際には IntelliJ IDEA 2017.3 より新しいバージョンを使うことを推奨します。
プロジェクトで使用している junit-platform-launcher, junit-jupiter-engine, junit-vintage-engine
のバージョンに対応した jar ファイルを自動的にダウンロードして使用するようになっているからです。
2017.3 より前のバージョンの IntelliJ IDEA は特定バージョンの JUnit 5 が同梱しています。 ですから、それより新しいバージョンの JUnit Jupiter でテストを実行すると、異なるバージョンのライブラリが同時に存在することになり、衝突してしまうからです。 そういう場合は、次の手順に従って、IntelliJ IDEA の同梱する JUnit 5 より新しいバージョンを使うようにしてください。 |
IDE の同梱する JUnit 5 と異なるバージョン(例えば 5.9.0-SNAPSHOT)を使用するには、対応するバージョンの junit-platform-launcher, junit-jupiter-engine, junit-vintage-engine
の jar ファイルをクラスパスに配置しなければなりません。
testImplementation(platform("org.junit:junit-bom:5.9.0-SNAPSHOT"))
// Only needed to run tests in a version of IntelliJ IDEA that bundles older versions
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
<!-- ... -->
<dependencies>
<!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.9.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
4.1.2. Eclipse
Eclipse IDE は Eclipse Oxygen.1a (4.7.1a) から JUnit Platform でテストを実行できるようになりました。
詳しい使い方は公式ドキュメントの Eclipse Project Oxygen.1a (4.7.1a) - New and Noteworthy Eclipse support for JUnit 5 を参照してください。
4.1.3. NetBeans
NetBeans は Apache NetBeans 10.0 release から JUnit Platform で JUnit Jupiter のテストを実行できるようになりました。
詳しい使い方は公式ドキュメントの https://netbeans.apache.org/download/nb100/index.html#junit_5[Apache NetBeans 10.0 release notes] _JUnit 5 を参照してください。
4.1.4. Visual Studio Code
Visual Studio Code では、 Java Test Runner 拡張 を使うと JUnit Platform で JUnit Jupiter のテストを実行できます。 Java Test Runner 拡張は Java Extension Pack の一部として一緒にインストールできます。
詳しい使い方は公式ドキュメントの https://code.visualstudio.com/docs/languages/java#testing[Java in Visual Studio Code] _Testing を参照してください。
4.1.5. 他の IDE
前述した以外のエディターや IDE を使用する場合のため、JUnit チームは JUnit 5 の使用を支援する2種類の方法を提供しています。 1つは コンソールランチャー をコマンドラインで実行する方法です。 もう1つは JUnit 4 based Runner を実行する方法で、こちらはエディタや IDE が JUnit 4 に対応している場合に使用できる方法です。
4.2. ビルドツールでテストを実行する
4.2.1. Gradle
JUnit Platform Gradle プラグインの開発は修了しました
JUnit チームの開発していた |
Gradle は 4.6 から JUnit Platform で 直接的にテストを実行できる ようになりました。
この機能を使用するには build.gradle
の test
タスクで useJUnitPlatform()
を呼び出すようにします。
test {
useJUnitPlatform()
}
test {
useJUnitPlatform {
includeTags("fast", "smoke & feature-a")
// excludeTags("slow", "ci")
includeEngines("junit-jupiter")
// excludeEngines("junit-vintage")
}
}
使用可能な全ての設定項目については 公式ドキュメント を参照してください。
設定パラメータの構成
現在の Gradle の標準 test
タスクには、JUnit Platform がテストを発見したり実行したりするための 設定パラメータ を指定する DSL が存在しません。
代わりにシステムプロパティや junit-platform.properties
ファイルで指定します。
test {
// ...
systemProperty("junit.jupiter.conditions.deactivate", "*")
systemProperty("junit.jupiter.extensions.autodetection.enabled", true)
systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_class")
// ...
}
テスト実行エンジンの構成
1度に複数のテストを実行するには、クラスパスに TestEngine
の実装クラスが存在しなければなりません。
JUnit Jupiter で作成したテストを実行するには、JUnit Jupiter の集約アーティファクトを testImplementation
依存関係で指定しなければなりません。
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.0-SNAPSHOT")
}
JUnit 4 で作成したテストを JUnit Platform で実行するには、JUnit 4 のアーティファクトを testImplementation
依存関係に指定するだけでなく、JUnit Vintage のアーティファクトを testRuntimeOnly
依存関係に指定しなければなりません。
dependencies {
testImplementation("junit:junit:4.13.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.9.0-SNAPSHOT")
}
ログ出力の設定(任意)
JUnit は警告情報やデバッグ情報を出力するため、Java の標準ログ API (java.util.logging
パッケージ、JUL と呼ばれる)を使用します。
LogManager
の設定方法について詳しくは 公式ドキュメント を参照してください。
JUL のログ出力は Log4j や Logback のようなロギングフレームワークへ転送できます。
使用するロギングフレームワークが LogManager
の実装クラスを提供している場合、システムプロパティの java.util.logging.manager
へ 完全修飾クラス名 を指定してください。
Log4j 2.x を使用する場合は次のように記述します(詳しくは Log4j JDK Logging Adapter を参照)。
test {
systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
}
ロギングフレームワークとして Logback を使用するなら、runtime
依存関係に JUL to SLF4J Bridge を追加するといいでしょう。
4.2.2. Maven
JUnit Platform Maven Surefire Provider プラグインの開発は修了しました
JUnit チームの開発していた |
Maven Surefire および Maven Failsafe では 2.22.0 から JUnit Platform で 直接的にテストを実行できる ようになりました。
junit5-jupiter-starter-maven
プロジェクトの pom.xml
ファイルを見ると、Maven Surefire プラグインをどのように設定するのか分かります。
テスト実行エンジンの構成
Maven Surefire や Maven Failsafe で1度に複数のテストを実行するには、クラスパスに TestEngine
の実装クラスが存在しなければなりません。
JUnit Jupiter で作成したテストを実行するには、JUnit Jupiter API と JUnit Jupiter TestEngine
(の実装クラス)の依存ライブラリを test
スコープで追加しなければなりません。
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
<!-- ... -->
JUnit 4 で作成したテストを JUnit Jupiter で作成したテストと一緒に実行するには、JUnit 4 と JUnit Vintage TestEngine
(の実装クラス)の依存ライブラリを test
スコープで追加しなければなりません。
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.9.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
<!-- ... -->
名前でテストクラスを選択する
Maven Surefire プラグインは、完全修飾クラス名が次のようなパス文字列パターンに一致するテストクラスを探索します。
-
**/Test*.java
-
**/*Test.java
-
**/*Tests.java
-
**/*TestCase.java
さらに、初期設定では全ての入れ子クラス(static
メンバークラスも含めて)を無視します。
しかし、この振る舞いは明示的に pom.xml
へ include
ルールと exclude
ルールを追加することで変更できます。
例えば、static
メンバークラスを無視する振る舞いを止めさせるには次のように exclude
ルールを記述します。
exclude
ルールを上書きする<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<excludes>
<exclude/>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
詳しくは公式ドキュメントの Inclusions and Exclusions of Tests を参照してください。
タグでテストクラスを選択する
-
タグ や タグ記法 を含むテストクラスを選択するには
groups
を使用します -
タグ や タグ記法 を含むテストクラスを除外するには
excludedGroups
を使用します
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<groups>acceptance | !feature-a</groups>
<excludedGroups>integration, regression</excludedGroups>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
設定パラメータの構成
JUnit Platform がテストを発見したり実行したりするための 設定パラメータ は configurationParameters
で指定します。
configurationProperties
には、 Java の Properties
記法でキーと値の対を記述します。
junit-platform.properties
ファイルでも指定できます。
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<properties>
<configurationParameters>
junit.jupiter.conditions.deactivate = *
junit.jupiter.extensions.autodetection.enabled = true
junit.jupiter.testinstance.lifecycle.default = per_class
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
4.2.3. Ant
Starting with version 1.10.3
of Ant, a new
junitlauncher
task has
been introduced to provide native support for launching tests on the JUnit Platform. The
junitlauncher
task is solely responsible for launching the JUnit Platform and passing
it the selected collection of tests. The JUnit Platform then delegates to registered test
engines to discover and execute the tests.
The junitlauncher
task attempts to align as close as possible with native Ant
constructs such as
resource collections
for allowing users to select the tests that they want executed by test engines. This
gives the task a consistent and natural feel when compared to many other core Ant tasks.
Starting with version 1.10.6
of Ant, the junitlauncher
task supports
forking the tests in a separate JVM.
The build.xml
file in the junit5-jupiter-starter-ant
project demonstrates how to use
the task and can serve as a starting point.
Basic Usage
The following example demonstrates how to configure the junitlauncher
task to select a
single test class (i.e., org.myapp.test.MyFirstJUnit5Test
).
<path id="test.classpath">
<!-- The location where you have your compiled classes -->
<pathelement location="${build.classes.dir}" />
</path>
<!-- ... -->
<junitlauncher>
<classpath refid="test.classpath" />
<test name="org.myapp.test.MyFirstJUnit5Test" />
</junitlauncher>
The test
element allows you to specify a single test class that you want to be selected
and executed. The classpath
element allows you to specify the classpath to be used to
launch the JUnit Platform. This classpath will also be used to locate test classes that
are part of the execution.
The following example demonstrates how to configure the junitlauncher
task to select
test classes from multiple locations.
<path id="test.classpath">
<!-- The location where you have your compiled classes -->
<pathelement location="${build.classes.dir}" />
</path>
<!-- ... -->
<junitlauncher>
<classpath refid="test.classpath" />
<testclasses outputdir="${output.dir}">
<fileset dir="${build.classes.dir}">
<include name="org/example/**/demo/**/" />
</fileset>
<fileset dir="${some.other.dir}">
<include name="org/myapp/**/" />
</fileset>
</testclasses>
</junitlauncher>
In the above example, the testclasses
element allows you to select multiple test
classes that reside in different locations.
For further details on usage and configuration options please refer to the official Ant
documentation for the
junitlauncher
task.
4.3. コンソールランチャー
ConsoleLauncher
はコマンドラインから JUnit Platform を実行するための Java アプリケーションです。
JUnit Jupiter のテストを実行した結果をコンソールに出力できるし、JUnit Vintage のテスト実行エンジンも使用できます。
依存ライブラリを一緒にした実行可能 jar ファイル junit-platform-console-standalone-1.9.0-SNAPSHOT.jar
は Maven Central で公開されています。
JUnit Platform リポジトリの junit-platform-console-standalone ディレクトリを参照してください。
jar ファイルは次のように実行します。
java -jar junit-platform-console-standalone-1.9.0-SNAPSHOT.jar <コンソールランチャーの引数(フラグ)>
実行結果は次のようになるでしょう。
├─ JUnit Vintage │ └─ example.JUnit4Tests │ └─ standardJUnit4Test ✔ └─ JUnit Jupiter ├─ StandardTests │ ├─ succeedingTest() ✔ │ └─ skippedTest() ↷ for demonstration purposes └─ A special test case ├─ Custom test name containing spaces ✔ ├─ ╯°□°)╯ ✔ └─ 😱 ✔ Test run finished after 64 ms [ 5 containers found ] [ 0 containers skipped ] [ 5 containers started ] [ 0 containers aborted ] [ 5 containers successful ] [ 0 containers failed ] [ 6 tests found ] [ 1 tests skipped ] [ 5 tests started ] [ 0 tests aborted ] [ 5 tests successful ] [ 0 tests failed ]
終了コード
テストコンテナやテストが1つでも失敗すると ConsoleLauncher はステータスコード 1 で終了します。
コマンドラインオプションで --fail-if-no-tests フラグを指定した場合、1つもテストが見つからないときは、ステータスコード 2 で終了します。
それ以外の場合、ステータスコード 0 で終了します。
|
4.3.1. コンソールランチャーの引数(フラグ)
Usage: ConsoleLauncher [-h] [--disable-ansi-colors] [--disable-banner] [--fail-if-no-tests] [--scan-modules] [--scan-classpath[=PATH[;|: PATH...]]]... [--details=MODE] [--details-theme=THEME] [--reports-dir=DIR] [-c=CLASS]... [--config=KEY=VALUE]... [-cp=PATH [;|:PATH...]]... [-d=DIR]... [-e=ID]... [-E=ID]... [--exclude-package=PKG]... [-f=FILE]... [--include-package=PKG]... [-m=NAME]... [-n=PATTERN]... [-N=PATTERN]... [-o=NAME]... [-p=PKG]... [-r=RESOURCE]... [-t=TAG]... [-T=TAG]... [-u=URI]... Launches the JUnit Platform from the console. -h, --help Display help information. --disable-ansi-colors Disable ANSI colors in output (not supported by all terminals). --disable-banner Disable print out of the welcome message. --details=MODE Select an output details mode for when tests are executed. Use one of: none, summary, flat, tree, verbose. If 'none' is selected, then only the summary and test failures are shown. Default: tree. --details-theme=THEME Select an output details tree theme for when tests are executed. Use one of: ascii, unicode. Default: unicode. -cp, --classpath, --class-path=PATH[;|:PATH...] Provide additional classpath entries -- for example, for adding engines and their dependencies. This option can be repeated. --fail-if-no-tests Fail and return exit status code 2 if no tests are found. --reports-dir=DIR Enable report output into a specified local directory (will be created if it does not exist). --scan-modules EXPERIMENTAL: Scan all resolved modules for test discovery. -o, --select-module=NAME EXPERIMENTAL: Select single module for test discovery. This option can be repeated. --scan-classpath, --scan-class-path[=PATH[;|:PATH...]] Scan all directories on the classpath or explicit classpath roots. Without arguments, only directories on the system classpath as well as additional classpath entries supplied via -cp (directories and JAR files) are scanned. Explicit classpath roots that are not on the classpath will be silently ignored. This option can be repeated. -u, --select-uri=URI Select a URI for test discovery. This option can be repeated. -f, --select-file=FILE Select a file for test discovery. This option can be repeated. -d, --select-directory=DIR Select a directory for test discovery. This option can be repeated. -p, --select-package=PKG Select a package for test discovery. This option can be repeated. -c, --select-class=CLASS Select a class for test discovery. This option can be repeated. -m, --select-method=NAME Select a method for test discovery. This option can be repeated. -r, --select-resource=RESOURCE Select a classpath resource for test discovery. This option can be repeated. -n, --include-classname=PATTERN Provide a regular expression to include only classes whose fully qualified names match. To avoid loading classes unnecessarily, the default pattern only includes class names that begin with "Test" or end with "Test" or "Tests". When this option is repeated, all patterns will be combined using OR semantics. Default: [^(Test.*|.+[.$] Test.*|.*Tests?)$] -N, --exclude-classname=PATTERN Provide a regular expression to exclude those classes whose fully qualified names match. When this option is repeated, all patterns will be combined using OR semantics. --include-package=PKG Provide a package to be included in the test run. This option can be repeated. --exclude-package=PKG Provide a package to be excluded from the test run. This option can be repeated. -t, --include-tag=TAG Provide a tag or tag expression to include only tests whose tags match. When this option is repeated, all patterns will be combined using OR semantics. -T, --exclude-tag=TAG Provide a tag or tag expression to exclude those tests whose tags match. When this option is repeated, all patterns will be combined using OR semantics. -e, --include-engine=ID Provide the ID of an engine to be included in the test run. This option can be repeated. -E, --exclude-engine=ID Provide the ID of an engine to be excluded from the test run. This option can be repeated. --config=KEY=VALUE Set a configuration parameter for test discovery and execution. This option can be repeated.
4.3.2. コンソールランチャーの引数(@-files)
OSによっては、実行するコマンドライン文字列の長さが上限値に達してしまう場合があります。
JUnit Platform 1.3 から ConsoleLauncher
は 引数ファイル を渡せるようになりました(@-files と呼ばれています)。
引数ファイルはコマンドライン引数を羅列したテキストファイルです。
この機能は picocli のコマンドライン文字列パーサーで実装されており、コマンドライン引数に @
で始まる項目があれば、対応するファイルの内容をコマンドライン引数に追加するようになっています。
引数ファイルでは、それぞれの引数を空白文字や改行文字で分割します。
引数自体に空白文字を含めるには、その引数全体を一重引用符や二重引用符で囲まなければなりません。
例えば "-f=My Files/Stuff.java"
のように記述します。
指定した引数ファイルが存在しないか、読み取れない場合、ただの1引数として扱います。
おそらく「使用できない引数」という警告メッセージが出力されるでしょう。
コマンドを実行するとき、システムプロパティ picocli.trace
に DEBUG
を指定すると問題の解決に役立つ情報が出力されるでしょう。
複数の引数ファイルを使用するときはコマンドライン引数に @-files を複数指定します。 ファイル名を完全パスで指定した場合以外は、実行時ディレクトリからの相対パスとして解釈します。
@
から始まるフラグ名を使いたいときは、@@
のように2つ続けて記述します。
例えば @@somearg
は @somearg
として扱われるようになります(引数ファイルとして展開しません)。
4.4. JUnit 4 で JUnit Platform を実行する
JUnitPlatform ランナーは廃止予定ですJUnit チームが ここ数年の間に、さまざまなビルドツールや IDE は JUnit Platform を直接的に実行できるようになりました。 また、 以上の理由により、JUnit Platform 1.8 から
|
JUnitPlatform
ランナーは、JUnit Platform の提供するプログラミングモデルで作成したテストを、JUnit 4 の環境で実行するテストランナーです。
テストクラスを @RunWith(JUnitPlatform.class)
で修飾すれば、JUnit Platform には対応していないけど JUnit 4 に対応した IDE やビルドツールからテストを実行できるようになります。
JUnit Platform には JUnit 4 にない機能が存在します。
したがって JUnitPlatform ランナーは JUnit Platform の提供する機能の一部だけしか対応していません。
特にテスト結果のレポート機能には違いがあります(表示名と部品名 を参照)。
|
4.4.1. 準備
JUnitPlatform
ランナーを使用するには以降で説明するアーティファクトや依存ライブラリをクラスパスに配置しなければなりません。
具体的なグループID、アーティファクトID、バージョンについては 依存ライブラリに関するメタデータ を参照してください。
4.4.2. 表示名と部品名
@RunWith(JUnitPlatform.class)
で修飾したテストクラスを実行するときの 表示名 を変更するには、テストクラスを @SuiteDisplayName
で修飾して、設定します。
初期設定では、テストアーティファクトの 表示名 を使うようになっています。
しかし、Gradle や Maven 等のビルドツールで JUnitPlatform
ランナーを実行して、テスト結果のレポートを作成するときは、テストアーティファクトの 部品名 が必要になります。
例えば、テストクラスの省略形(表示名)や特殊文字を含む表示名ではなく、完全修飾クラス名が必要になる、といった具合です。
テスト結果のレポートを作成するときに部品名を使用できるようにするには、テストクラスを @UseTechnicalNames
で修飾してください。
テストクラスを @UseTechnicalNames
で修飾すると、@SuiteDisplayName
で指定した表示名を上書きします。
4.4.3. 単独のテストクラス
テストクラスを @RunWith(JUnitPlatform.class)
で修飾するのは JUnitPlatform
ランナーを使用する方法の1つです。
次のコード例ではテストメソッドを JUnit Jupiter の @Test
で修飾しているのがポイントです( org.junit.Test
ではなく org.junit.jupiter.api.Test
を使用しています)。
さらに、テストクラスの可視性が public
になっているのもポイントです。
そうしないと JUnit 4 のテストクラスとして認識しない IDE やビルドツールがあるからです。
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
@RunWith(org.junit.platform.runner.JUnitPlatform.class)
public class JUnitPlatformClassDemo {
@Test
void succeedingTest() {
/* no-op */
}
@Test
void failingTest() {
fail("Failing for failing's sake.");
}
}
4.4.4. テストスイート
次のコード例のように、複数のテストクラスをまとめたテストスイートがあるとします。
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.SuiteDisplayName;
import org.junit.runner.RunWith;
@RunWith(org.junit.platform.runner.JUnitPlatform.class)
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
public class JUnitPlatformSuiteDemo {
}
JUnitPlatformSuiteDemo
クラスは example
パッケージとそのサブパッケージに配置した全てのテストクラスを発見し、実行します。
初期設定では Test
で始まるか、Test
や Tests
で終わる名前のクラスが対象になります。
さらに細かい設定
@SelectPackages 以外にもテストを発見したり選択したりするための設定項目があります。
詳しくは org.junit.platform.suite.api の Javadoc を参照してください。
|
@RunWith(JUnitPlatform.class) で修飾したテストスイートクラスやテストクラスは JUnit Platform から直接的に実行 できません (IDE のドキュメントに記載されている "JUnit 5" テストとしても実行できません)。
JUnit 4 でしか実行できません。
|
4.5. 設定パラメータの構成
テストクラスの選択や、テスト実行エンジンの指定、テストクラスを探索するパッケージの指定といった基盤機能の説明に加えて、特定のテスト実行エンジンやリスナー、拡張機能に固有の設定パラメータについても理解しておいたほうがいいでしょう。 例えば、JUnit Jupiter テスト実行エンジンには次のようなユースケースを想定した設定パラメータが用意されています。
設定パラメータ は文字列ベースのキーと値の対の集合です。 JUnit Platform を実行するときは次のように注入します。
-
Launcher
API のパラメータを構築するLauncherDiscoveryRequestBuilder
のconfigurationParameter()
メソッドやconfigurationParameters()
メソッドを使用します。 JUnit Platform を実行するツールによって渡し方は異なります。-
コンソールランチャー:
--config
フラグを使用します。 -
設定パラメータの構成: DSL を使用します(
systemProperty
やsystemProperties
)。 -
設定パラメータの構成:
configurationParameters
要素を使用します。
-
-
JVM のシステムプロパティを使用します。
-
JUnit Platform の独自設定ファイルを使用します。Java の
Properties
ファイルとして作成したjunit-platform.properties
をクラスパスの最上位に配置します。
設定パラメータを読み取る順序は固定されています。
つまり、Launcher に直接指定したパラメータは、システムプロパティや設定ファイルなど他の方法で指定したパラメータを上書きするのです。
同様に、システムプロパティで指定したパラメータは、設定ファイルで指定したパラメータを上書きします。
|
4.5.1. パターンマッチ記法
このセクションでは、次のような機能のために 設定パラメータ へ記述するパターンマッチの記法を説明します。
設定値の文字列パターンがアスタリスク * 1つだけなら、全ての候補クラスにマッチします。
そうでなければ、設定値をカンマ区切り文字列(それぞれの文字列パターンは完全修飾クラス名(FQCN))と見做して、それぞれの文字列パターンと候補クラスが一致するか評価します。
文字列パターン中のドット(.
)は、FQCN 中のドット(.
)か、$マーク($
)とマッチします。
文字列パターン中のアスタリスク(*)は、FQCN 中の連続する任意の1文字以上の文字列とマッチします。
それ以外の文字は、FQCN 中の同じ文字とマッチします。
具体例です。
-
*: 全ての候補クラスとマッチします
-
org.junit.*:
org.junit
パッケージとそのサブパッケージに配置した全ての候補クラスとマッチします any of its subpackages. -
{asterrisk}.MyCustomImpl: 単純クラス名が
MyCustomImpl
になる全ての候補クラスとマッチします -
{asterrisk}System{asterrisk}: FQCN に
System
を含む全ての候補クラスとマッチします -
{asterrisk}System{asterrisk}, {asterrisk}Unit{asterrisk}: FQCN に
System
あるいはUnit
を含む全ての候補クラスとマッチします -
org.example.MyCustomImpl
: FQCN が完全にorg.example.MyCustomImpl
と一致する候補クラスとマッチします -
org.example.MyCustomImpl, org.example.TheirCustomImpl
: FQCN が完全にorg.example.MyCustomImpl
と一致する、あるいはorg.example.TheirCustomImpl
と一致する候補クラスとマッチします
4.6. タグ
タグはテストクラスに印を付けたり、テストクラスを選択するための機能です。
テストコンテナやテストクラスにタグを追加するためのプログラミングモデルは、テストフレームワークが提供しています。
例えば、JUnit Jupiter でテストを作成するなら @Tag
アノテーション(タグ付けと選択 を参照)を使用し、JUnit 4 で作成したテストなら Vintage テスト実行エンジンが @Category
アノテーションをタグに写像します(カテゴリー(Categories)の対応 を参照)。
他のテストフレームワークも、ユーザーがタグを定義するために使用できる独自のアノテーションを提供しているでしょう。
4.6.1. タグの構文規則
タグを指定する方法がなんであれ、JUnit Platform は次の規則で解釈します。
-
null
や 空白文字 は使用できません -
トリムしたタグ値には、空白文字を使用できません
-
トリムしたタグ値には、ISO 制御文字を使用できません
-
トリムしたタグ値には、次のいずれかの文字(予約文字)を使用できません
-
,
: カンマ -
(
: 開き括弧 -
)
: 閉じ括弧 -
&
: アンパサンド -
|
: 縦線 -
!
: 感嘆符
-
In the above context, "trimmed" means that leading and trailing whitespace characters have been removed. NOTE: 「トリムした」というのは、文字列の先頭や末尾から連続する1文字以上の空白文字を削除するということです。 |
4.6.2. タグ記法
タグ記法は !
と &
と |
を演算子として使用するブール式です。
(
と )
で演算子の優先度を変更できるようになっています。
特殊記法として any()
と none()
を使用できます。
前者はあらゆるタグと 一致する ことを評価し、後者はあらゆるタグと 一致しない ことを評価します。
これらの特殊記法は通常のタグ記法と組み合わせて使用できます。
演算子 | 意味 | 結合の方向 |
---|---|---|
|
否定 |
右 |
|
かつ |
左 |
|
あるいは |
左 |
テストを複数の軸でタグ付けしているときは、タグ記法を使うと実行するテストを選択するのに役立ちます。 タグでテスト種類(micro、integration、end-to-end など)や機能(product、catalog、shipping など)を説明しているときは、次のように使用できます。
タグ記法 |
選択されたテストクラス |
product |
product を指定したテストクラス |
catalog | shipping |
catalog あるいは shipping を指定したテストクラス |
catalog & shipping |
catalog と shipping を指定したテストクラス |
product & !end-to-end |
product を指定したテストクラスの中で end-to-end を指定していないもの |
(micro | integration) & (product | shipping) |
micro あるいは integration を指定したテストクラスの中で product あるいは shipping を指定しているもの |
4.7. 標準出力や標準エラー出力をキャプチャーする
JUnit Platform は 1.3 から System.out
と System.err
に出力した内容をキャプチャーする機能をオプトイン方式で提供しています。
この機能を有効化するには、システムプロパティや設定ファイル(設定パラメータの構成 を参照)で junit.platform.output.capture.stdout
あるいは junit.platform.output.capture.stderr
に true
を設定します。
また、実行時の出力バッファサイズをテストコンテナやテストクラスごとに変更するときは junit.platform.output.capture.maxBuffer
を設定します。
この機能を有効化すると、JUnit Platform は標準出力(標準エラー出力)をキャプチャーします。
キャプチャーした内容は、登録済みの TestExecutionListener
へ発行するレポート情報のキー stdout
(stderr
)で参照できます。
リスナーインスタンスは、テストコンテナやテストクラスの実行が完了する前に、レポート情報を参照できます。
なお、テストコンテナやテストクラスを実行したスレッドが標準出力(標準エラー出力)に出力した内容しかキャプチャーできません。 他のスレッドが出力した内容は無視されてしまいますし、特に 並列実行 させている場合は、どのテストコンテナやテストクラスが出力した内容なのか、後から区別できません。
4.8. リスナーを使用する
JUnit Platform の提供するリスナーAPIを使用すると、TestPlan
がテストを発見したり実行したりする途中のさまざまなポイントで発火するイベントに、JUnit やサードパーティライブラリだけでなく、ユーザーの作成したコードが反応できるようになります。
-
LauncherSessionListener
:LauncherSession
が開かれた、あるいは、閉じられたイベントを受信します -
LauncherDiscoveryListener
: テストの探索中に発生したイベントを受信します -
TestExecutionListener
: テストの実行中に発生したイベントを受信します
基本的に LauncherDiscoveryListener
API を使用するのはビルドツールや IDE が独自の機能を提供するためです。
テスト実行時に自動的に登録する場合が多いでしょう。
LauncherDiscoveryListener
と TestExecutionListener
API を使用することが多いのは、任意の形式のテスト結果レポートを生成したり、IDE 内の GUI でテストの実行計画を表示したりする場合です。
やはりビルドツールや IDE の提供する実装クラスを実行時に自動的に登録する場合が多いでしょう。
サードパーティライブラリを含む場合もあるようです(いずれにしても自動的に登録するようになっています)。
独自のリスナーを実装、登録することもできます。
リスナーを登録したり構成したりする方法については次のドキュメントを参照してください。
JUnit Platform は次のようなリスナーを提供しています。
- Java Flight Recorder を使用する
-
FlightRecordingExecutionListener
とFlightRecordingDiscoveryListener
。 テストの探索時と実行時に Java Flight Recorder のイベントを発行します。 LegacyXmlReportGeneratingListener
-
TestExecutionListener
が、JUnit 4 のテスト結果レポート形式として事実上の標準になっている XML 形式のテスト結果レポートを生成します。 詳しくは JUnit Platform レポート機能 を参照してください。 LoggingListener
-
TestExecutionListener
の生成する全てのログメッセージをBiConsumer
の消費するイベントとして生成します(Throwable
とSupplier<String>
として渡します)。 SummaryGeneratingListener
-
TestExecutionListener
が実行したテストの概要をPrintWriter
で(標準出力に)出力します。 UniqueIdTrackingListener
-
TestExecutionListener
の実行するTestPlan
がスキップおよび実行した全てのテストそれぞれに固有の ID を割り当てて追跡し、TestPlan
が完了したら追跡した全ての ID をファイルに記録します。
4.8.1. Java Flight Recorder を使用する
JUnit Platform は 1.7 から Java Flight Recorder のイベントを生成する機能をオプトイン方式で提供しています。 JEP 328 は Java Flight Recorder(JFR)を次のように説明しています。
Flight Recorder はアプリケーション(JVM や OS)の生成したイベントを記録します。 イベントは単一ファイルへ記録します。 イベントを記録したファイルはバグ報告へ添付したり、サポートエンジニアが調査するために使用したりできるので、問題が発生するまでに起きていたことを後から分析できます。 |
テストを実行しながら Flight Recorder イベントを記録するには次のような設定をしなければなりません。
-
Java 8 Update 262 以降、あるいは Java 11 以降を使用します。
-
org.junit.platform.jfr
モジュール(junit-platform-jfr-1.9.0-SNAPSHOT.jar
)を、テストランタイムの参照できるクラスパスやモジュールパスへ配置します。 -
テストを実行するときにフライトレコード機能を有効化します。具体的には JVM のコマンドライン引数に次のようなフラグを指定します。
-XX:StartFlightRecording:filename=...
適切なコマンドライン引数を構成するには、使用しているビルドツールのドキュメントを参照してください。
JDK と一緒に配布されているコマンドラインツールの jfr や、https://jdk.java.net/jmc/[JDK Mission Control] を使用すれば、記録したイベントを分析できます。
今のところ Flight Recorder のイベント記録機能は 実験的な機能です。 JUnit チームがこの機能を改善して、正式機能へ 昇格 できるよう、試用してフィードバックを提供してください。 |
5. 拡張モデル
5.1. 概要
JUnit 4 の拡張ポイントである Runner
や TestRule
や MethodRule
に比べると、JUnit Jupiter の拡張モデル Extension
API は単純で分かりやすい考え方になっています。
なお、Extension
自体はただのマーカーインターフェイスです。
5.2. 拡張機能を登録する
拡張機能を登録する方法は3種類あります。
@ExtendWith
を使用する 宣言的な 登録と、@RegisterExtension
を使用する 手続き的な 登録と、Java の ServiceLoader
を使用する 自動的な 登録です。
5.2.1. 宣言的に登録する方法
開発者は @ExtendWith(…)
に(あるいは自作の 合成アノテーション に)登録する拡張機能のクラス参照を指定することで、テストインターフェイスやテストクラスやテストメソッドへ1つ以上の拡張機能を登録できます。
JUnit Jupiter の 5.8 からは、フィールドや、テストクラスのコンストラクタあるいはテストメソッドあるいはライフサイクルメソッドのメソッド引数を @ExtendWith
で修飾できるようになりました。
例えば、WebServerExtension
をテストメソッドへ登録するときは次のように記述します。
ここでは、WebServerExtension
が HTTP サーバーを起動し、@WebServerUrl
で修飾したメソッド引数へサーバーのURLを注入することを想定しています。
@Test
@ExtendWith(WebServerExtension.class)
void getProductList(@WebServerUrl String serverUrl) {
WebClient webClient = new WebClient();
// Use WebClient to connect to web server using serverUrl and verify response
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
あるクラスやそのサブクラスに定義した全てのテストメソッドで使用するために WebServerExtension
を登録するには、テストクラスを次のように修飾します。
@ExtendWith(WebServerExtension.class)
class MyTests {
// ...
}
@ExtendWith
には複数の拡張機能(クラス参照)を登録できます。
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
class MyFirstTests {
// ...
}
それぞれの拡張機能ごとに @ExtendWith
を記述することもできます。
@ExtendWith(DatabaseExtension.class)
@ExtendWith(WebServerExtension.class)
class MySecondTests {
// ...
}
拡張機能の登録順序
クラスやメソッドやメソッド引数について宣言的に登録した拡張は、ソースコードに記述したとおりの順番で登録されます。
例えば、前のコード例の |
複数の拡張機能の組み合わせを再利用できるようにしたければ、@ExtendWith
を メタアノテーション として使用する独自の 合成アノテーション を作成すればいいでしょう。
そうすれば、@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
を @DatabaseAndWebServerExtension
に置き換えられます。
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
public @interface DatabaseAndWebServerExtension {
}
前の例ではクラスやメソッドを @ExtendWith
で修飾していました。
しかし、フィールドやメソッド引数に対して拡張機能を登録するほうが役に立つ場合があるのです。
乱数を生成する拡張機能があるとして、生成した乱数をフィールドやコンストラクタ引数、テストメソッドの引数、ライフサイクルメソッドの引数に注入できる場合を考えてみましょう。
なお、この拡張機能は @ExtendWith(RandomNumberExtension.class)
をメタアノテーションとする @Random
アノテーションを提供するものとします。
この拡張機能は次のように使用できるでしょう。
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}
class RandomNumberDemo {
// use random number field in test methods and @BeforeEach
// or @AfterEach lifecycle methods
@Random
private int randomNumber1;
RandomNumberDemo(@Random int randomNumber2) {
// use random number in constructor
}
@BeforeEach
void beforeEach(@Random int randomNumber3) {
// use random number in @BeforeEach method
}
@Test
void test(@Random int randomNumber4) {
// use random number in test method
}
}
フィールドを
@ExtendWith で修飾した場合の拡張機能の登録順序フィールドを |
@ExtendWith は static 宣言したフィールドとしなかったフィールドのどちらも修飾できます。
@RegisterExtension で修飾するフィールドを static 宣言する場合 と static 宣言しない場合 と同じ規則になります。
|
5.2.2. 手続き的に登録する方法
開発者はテストクラスのフィールドを @RegisterExtension
で修飾することで、手続き的に拡張機能を登録できます。
@ExtendWith
で 宣言的に登録する 場合、アノテーションを使用するしか設定を変更する方法がありませんでした。
しかし、@RegisterExtension
で 手続き的に登録する 場合、拡張機能のコンストラクタや static
宣言したファクトリメソッドやビルダー API で設定を変更できます。
拡張機能の登録順序
初期設定では、
|
@RegisterExtension で修飾するフィールドは決して null にしてはいけません(参照するとき)。static 宣言したフィールドとしなかったフィールドのどちらも修飾できます。
|
static 宣言したフィールドで拡張機能を登録する
@RegisterExtension
で修飾するフィールドを static
宣言する場合、テストクラスを修飾する @ExtendWith
より後に拡張機能を登録します。
このように クラスレベルで登録した拡張機能 は、どのような拡張APIでも実装できることになっています(制限はありません)。
クラスレベルのAPIである BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor, TestInstancePreDestroyCallback
を実装できますし、メソッドレベルのAPIである BeforeEachCallback
なども実装できるのです。
次のコード例では、テストクラスのフィールド server
を、WebServerExtension
のビルダー API で手続き的に初期化しています。
WebServerExtension
は自動的にクラスレベルの拡張として登録されます。
ここでは、テストクラスの全てのテストを実行する前に HTTP サーバーを開始し、全てのテストが完了してから HTTP サーバーを停止することになります。
さらに、static
宣言したライフサイクルメソッド @BeforeAll
や @AfterAll
は、@BeforeEach
や @AfterEach
や @Test
と同じく server
フィールドを介して拡張機能のインスタンスを参照できます。
class WebServerDemo {
@RegisterExtension
static WebServerExtension server = WebServerExtension.builder()
.enableSecurity(false)
.build();
@Test
void getProductList() {
WebClient webClient = new WebClient();
String serverUrl = server.getServerUrl();
// Use WebClient to connect to web server using serverUrl and verify response
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
}
Kotlin における static フィールドの扱い
Kotlin には static
フィールドという考え方がありません。
しかし、@JvmStatic
アノテーションを使うと、コンパイラに private static
フィ-ルドを生成させられるようになっています。
public static
フィールドを生成させたいときは @JvmField
アノテーションを使用します。
次のコード例は前の WebServerDemo
の Kotlin バージョンです。
class KotlinWebServerDemo {
companion object {
@JvmStatic
@RegisterExtension
val server = WebServerExtension.builder()
.enableSecurity(false)
.build()
}
@Test
fun getProductList() {
// Use WebClient to connect to web server using serverUrl and verify response
val webClient = WebClient()
val serverUrl = server.serverUrl
assertEquals(200, webClient.get("$serverUrl/products").responseStatus)
}
}
インスタンスフィールドで拡張機能を登録する
@RegisterExtension
で修飾したフィールドがインスタンスフィールドの場合、テストクラスをインスタンス化し、登録済みの全ての TestInstancePostProcessor
が後処理を実行し(拡張機能が自身のインスタンスをフィールドに注入する可能性もあります)、それから拡張機能を登録します。
したがって インスタンスレベルの拡張機能 が BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor
のような API を実装していたとしても、呼び出される機会はありません。
初期設定ではインスタンスレベルの拡張機能が登録されるのは、メソッドを修飾した @ExtendWith
の拡張機能より 後 になります。
ただし、テストクラスのセマンティクスをクラス単位(per-class)にしている場合、メソッドを修飾した @ExtendWith
の拡張機能より 前 に登録します。
次のコード例ではインスタンスフィールド docs
を手続き的に初期化しています。
独自の lookUpDocsDir()
メソッドの返り値を DocumentExtension
のファクトリメソッド forPath()
の引数にしているのです。
DocumentationExtension
はメソッドレベルで自動的に登録される拡張機能ということになります。
さらに、@BeforeEach, @AfterEach, @Test
で修飾したメソッドから、docs
フィールドを介して拡張機能のインスタンスを参照できます。
class DocumentationDemo {
static Path lookUpDocsDir() {
// return path to docs dir
}
@RegisterExtension
DocumentationExtension docs = DocumentationExtension.forPath(lookUpDocsDir());
@Test
void generateDocumentation() {
// use this.docs ...
}
}
5.2.3. 自動的に拡張機能を登録する
JUnit Jupiter は 宣言的な拡張機能の登録方法 と 手続き的な拡張機能の登録方法 に加えて、Java の ServiceLoader
を使用する 大局的な拡張機能の登録方法 に対応しています。
この方法を使用すると、クラスパスに配置したサードパーティの拡張機能を自動的に検出、登録することができます。
具体的には、拡張機能の実装と /META-INF/services/org.junit.jupiter.api.extension.Extension
というファイルを jar ファイルに配置します。
org.junit.jupiter.api.extension.Extension
には拡張機能の実装クラスの完全修飾クラス名を記述します。
拡張機能の自動検出を有効化する
自動検出機能は高度な機能なので、初期設定では無効化されています。
有効化するには 設定パラメータ junit.jupiter.extensions.autodetection.enabled
に true
を設定します。
Launcher
に渡す LauncherDiscoveryRequest
の値を、システムプロパティや設定ファイル(設定パラメータの構成 を参照)で指定できます。
例えば、システムプロパティを指定するときは次のように実行します。
-Djunit.jupiter.extensions.autodetection.enabled=true
自動検出機能を有効にしている場合、JUnit Jupiter の基本拡張(TestInfo
や TestReporter
のための拡張機能)を登録した後に、ServiceLoader
の検出した拡張機能を登録します。
5.3. 条件付きのテスト実行
ExecutionCondition
は 条件付きのテスト実行 を手続き的に実現する拡張APIです。
ExecutionCondition
はそれぞれのテストコンテナ(テストクラス)ごとに 評価され、ExtensionContext
よりそのコンテナに含まれる全てのテストを実行するかどうかを決定します。
同じように、ExecutionCondition
はそれぞれのテストメソッドごとに 評価され、ExtensionContext
そのテストメソッドを実行するかどうかを決定します。
複数の ExecutionCondition
を登録した場合、どちらかの評価結果が disabled ならただちにそのテストコンテナ(あるいはテスト)を無効化します。
つまり、常に登録した全ての条件が評価されるわけではないということです(他の条件がテストを無効と判断しているかもしれません)。
ブール演算の OR 演算子のように短絡評価するのです。
具体例は DisabledCondition
や @Disabled
のソースコードを参照してください。
5.3.1. 条件を不活性にする
想定した条件を 満たさない場合 でもテストスイートを実行できるようになっていると便利なときがあります。
例えば、成功しないことが分かっている としても @Disabled
で修飾したテストを実行したい場合があるかもしれません。
そのためには、設定パラメータの junit.jupiter.conditions.deactivate
で不活性にする(評価しない)条件を指定します。
Launcher
に渡す LauncherDiscoveryRequest
の値を、システムプロパティや設定ファイル(設定パラメータの構成 を参照)で指定できます。
例えば、@Disabled
をシステムプロパティで不活性化するときは次のように実行します。
-Djunit.jupiter.conditions.deactivate=org.junit.\*DisabledCondition
パターンマッチ記法
パターンマッチ記法 を参照してください。
5.4. テストインスタンスファクトリ
TestInstanceFactory
はテストクラスのインスタンスを 作成する ための拡張APIです。
一般的な使い方として、DIフレームワークからテストインスタンスを取得したり、static
ファクトリメソッドでテストインスタンスを作成することがあると思います。
TestInstanceFactory
が登録されていなければ、テストフレームワークはテストクラスをインスタンス化するため 唯一のコンストラクタ を使用します。
ParameterResolver
でコンストラクタ引数を解決する場合があるかもしれません。
TestInstanceFactory
を実装した拡張機能を登録できるのは、テストインターフェイスやトップレベルクラス、@Nested
で修飾した内部クラスです。
1つのテストクラスに対して複数の |
5.5. テストインスタンスを作成した後の処理
TestInstancePostProcessor
はテストクラスのインスタンスを作成した 後 に実行する処理を実行するための拡張APIです。
一般的な使い方として、テストインスタンスに依存オブジェクトを注入したり、独自の初期化メソッドを呼び出すことがあると思います。
具体例は MockitoExtension
や SpringExtension
のソースコードを参照してください。
5.6. テストインスタンスを破棄する前のコールバック
TestInstancePreDestroyCallback
はテストクラスのインスタンスがテストを実行した 後、インスタンスを破棄する 前 に実行したい処理を実行するための拡張APIです。
一般的な使い方として、テストインスタンスの依存オブジェクトを後始末したり、独自の逆初期化メソッドを呼び出すことがあると思います。
5.7. パラメータの解決
ParameterResolver
は実行時にいろいろなパラメータを解決するための拡張APIです。
テストクラス のコンストラクタ、テストメソッド、ライフサイクルメソッド(テストクラスとテストメソッド を参照)に宣言した引数は、ParameterResolver
により実行時に 解決 しなければなりません。
使用できるのは組み込みの ParameterResolver
(TestInfoParameterResolver
など)か、ユーザーの登録した拡張機能 が提供する ParameterResolver
です。
一般的には、引数の 名前 や 型 や アノテーション、あるいはそれらの組み合わせから解決することになるでしょう。
総称型クラスのアダプターである TypeBasedParameterResolver
を継承すると、特定の型に対する引数を解決する ParameterResolver
を簡単に実装できます。
具体例は CustomTypeParameterResolver
や CustomAnnotationParameterResolver
や MapOfListsTypeBasedParameterResolver
のソースコードを参照してください。
JDK 9 より前の JDK に付属する
|
5.8. テスト結果の処理
TestWatcher
は テストメソッド の実行結果を処理するための拡張APIです。
例えば、TestWatcher
は実行時の状況に応じた次のようなイベントを受信します(メソッドを呼び出します)。
-
testDisabled
: 無効化された テストメソッド をスキップしたときに呼び出すメソッドです -
testSuccessful
: テストメソッド の実行が正常に終了した後に呼び出すメソッドです -
testAborted
: テストメソッド の実行が中断された後に呼び出すメソッドです -
testFailed
: テストメソッド の実行が失敗した後に呼び出すメソッドです
テストクラスとテストメソッド における「テストメソッド」の定義とは違って、このセクションの テストメソッド は @Test や @TestTemplate で修飾したメソッドのことを指しています(@RepeatedTest や @ParameterizedTest も含みます)。
|
TestWatcher
の実装クラスを提供する拡張機能は、メソッドレベルかクラスレベルのいずれかで登録します。
拡張機能をクラスレベルで登録した場合、そこに含まれる全てのテストメソッドと、@Nested
テストクラスに含まれる全てのテストメソッドが対象になります。
|
5.9. ライフサイクルコールバック
次のインターフェイスはテストの実行時ライフサイクルにおけるさまざまな時点で呼び出される拡張APIです。
詳しくは、後に続くセクションの具体例や、それぞれのインターフェイスや org.junit.jupiter.api.extension
パッケージの Javadoc を参照してください。
複数の拡張APIを実装する
拡張機能の作者は1つの実装クラスで複数の拡張API(インターフェイス)を実装できます。
具体例として SpringExtension のソースコードを参照してください。
|
5.9.1. テスト実行の前、および、後のコールバック
BeforeTestExecutionCallback
と AfterTestExecutionCallback
は、(相対的に)テストメソッドを実行する 直前 あるいは 直後 に任意の振る舞いを追加する拡張APIです。
時間を計測したり、トレースを記録したりするような場面で役立ちます。
@BeforeEach
メソッドの前後や、@AfterEach
メソッドの前後に振る舞いを追加したいときは、BeforeEachCallback
や AfterEachCallback
を実装します。
次のコード例では、テストメソッドの実行時間を計算してログに出力するため、TimingExtension
が BeforeTestExecutionCallback
と AfterTestExecutionCallback
を実装しています。
import java.lang.reflect.Method;
import java.util.logging.Logger;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final Logger logger = Logger.getLogger(TimingExtension.class.getName());
private static final String START_TIME = "start time";
@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
getStore(context).put(START_TIME, System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
Method testMethod = context.getRequiredTestMethod();
long startTime = getStore(context).remove(START_TIME, long.class);
long duration = System.currentTimeMillis() - startTime;
logger.info(() ->
String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
}
private Store getStore(ExtensionContext context) {
return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
}
}
TimingExtensionTests
では TimingExtension
を @ExtendWith
で登録しているので、それぞれのテストメソッドの実行時間が計測されています。
TimingExtension
を使用するテストクラス@ExtendWith(TimingExtension.class)
class TimingExtensionTests {
@Test
void sleep20ms() throws Exception {
Thread.sleep(20);
}
@Test
void sleep50ms() throws Exception {
Thread.sleep(50);
}
}
実行すると、次のような出力を得られるでしょう。
INFO: Method [sleep20ms] took 24 ms. INFO: Method [sleep50ms] took 53 ms.
5.10. 例外処理
テストを実行しているときに例外が発生したら、それを呼び出し元に伝播させる前に介入して、なんらかの処理をさせたい場合があります(エラーをログに出力したり、リソースを解法したり)。
JUnit Jupiter ではそのような拡張機能を作成するための拡張APIを提供しています。
@Test
メソッドで発生した例外を処理する TestExecutionExceptionHandler
と、ライフサイクルメソッド(@BeforeAll, @BeforeEach, @AfterEach, @AfterAll
)で発生した例外を処理する LifecycleMethodExecutionExceptionHandler
です。
次のコード例では、テストを実行しているときに発生した IOException
以外の例外を再スローする拡張機能を実装しています。
IOException
を隠す例外処理ハンドラーの拡張機能public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {
@Override
public void handleTestExecutionException(ExtensionContext context, Throwable throwable)
throws Throwable {
if (throwable instanceof IOException) {
return;
}
throw throwable;
}
}
次のコード例では、準備や後始末をしているときに予期せぬ例外が発生した場合、テスト対象のアプリケーションの状態を記録する方法を示しています。
ライフサイクルメソッドのコールバックを使用する場合と違って、例外処理ハンドラーを実行するかどうかは実行したテストの内容によって異なります。
この例のような使い方をすると、@BeforeALl, @BeforeEach, @AfterEach, @AfterAll
が失敗した直後に呼び出されることが保証されています。
class RecordStateOnErrorExtension implements LifecycleMethodExecutionExceptionHandler {
@Override
public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during class setup");
throw ex;
}
@Override
public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during test setup");
throw ex;
}
@Override
public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during test cleanup");
throw ex;
}
@Override
public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during class cleanup");
throw ex;
}
}
同じライフサイクルメソッドの定義順によって、複数の例外処理ハンドラーを呼び出す可能性があります。 いずれかの例外処理ハンドラーが自分で処理した例外を隠してしまうと、後に続く例外処理ハンドラーは呼び出されなくなりますし、JUnit にも失敗が伝わりません。 例外が発生しなかったとように扱われてしまうのです。 例外処理ハンドラーは受け取った例外を再スローするか、(受け取った例外を内包する)新しい例外をスローするか選ぶことになるでしょう。
@BeforeAll, @AfterAll
で発生した例外を、LifecycleMethodExecutionExceptionHandler
を実装した拡張機能で扱いたいときは、拡張機能をクラスレベルで登録しなければなりません。
この場合、それぞれのテストメソッドに対する BeforeEach, AfterEach
にも例外処理ハンドラーが登録されます。
// Register handlers for @Test, @BeforeEach, @AfterEach as well as @BeforeAll and @AfterAll
@ExtendWith(ThirdExecutedHandler.class)
class MultipleHandlersTestCase {
// Register handlers for @Test, @BeforeEach, @AfterEach only
@ExtendWith(SecondExecutedHandler.class)
@ExtendWith(FirstExecutedHandler.class)
@Test
void testMethod() {
}
}
5.11. テストコードの呼び出しへの割り込み
InvocationInterceptor
はテストコードの呼び出しに割り込んで処理をさせたいときに使用する拡張APIです。
次のコード例では、Swing のイベントディスパッチスレッドで全てのテストメソッドを実行しています。
public class SwingEdtInterceptor implements InvocationInterceptor {
@Override
public void interceptTestMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext,
ExtensionContext extensionContext) throws Throwable {
AtomicReference<Throwable> throwable = new AtomicReference<>();
SwingUtilities.invokeAndWait(() -> {
try {
invocation.proceed();
}
catch (Throwable t) {
throwable.set(t);
}
});
Throwable t = throwable.get();
if (t != null) {
throw t;
}
}
}
5.12. テストテンプレートに呼び出し元のコンテキストを与える
@TestTemplate
で修飾したメソッドを実行するには、少なくとも1つの TestTemplateInvocationContextProvider
を登録しなければなりません。
このインターフェイスの実装クラスは TestTemplateInvocationContext
を要素とする Stream
を提供しなければなりません。
それぞれの要素には、独自の表示名や、直後の @TestTemplate
メソッドを呼び出すときにだけ使用する拡張機能のリストが含まれています。
次のコード例では、テストテンプレートの書き方と、TestTemplateInvocationContextProvider
の実装方法を説明しています。
final List<String> fruits = Arrays.asList("apple", "banana", "lemon");
@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String fruit) {
assertTrue(fruits.contains(fruit));
}
public class MyTestTemplateInvocationContextProvider
implements TestTemplateInvocationContextProvider {
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext context) {
return Stream.of(invocationContext("apple"), invocationContext("banana"));
}
private TestTemplateInvocationContext invocationContext(String parameter) {
return new TestTemplateInvocationContext() {
@Override
public String getDisplayName(int invocationIndex) {
return parameter;
}
@Override
public List<Extension> getAdditionalExtensions() {
return Collections.singletonList(new ParameterResolver() {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(String.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameter;
}
});
}
};
}
}
前のコード例では、テストテンプレートは2回呼び出されるはずです。
それぞれの呼び出しについて、呼び出しコンテキストに指定された表示名は apple
と banana
になります。
また、メソッド引数を解決する独自の ParameterResolver
も登録されています。
ConsoleLauncher
でテストを実行すると、次のように出力されるでしょう。
└─ testTemplate(String) ✔ ├─ apple ✔ └─ banana ✔
TestTemplateInvocationContextProvider
API の主な目的は、さまざまなコンテキストで呼び出されるメソッドのように、繰り返し呼び出されることが前提の普通とは異なるテストを実現することです。
例えば、毎回違うパラメータで実行したり、テストクラスのインスタンスの状態を毎回変化させたり、状態を変化させずに何度も実行するなどです。
この API で実現されてる 繰り返しテスト や パラメタライズテスト についても参照してください。
5.13. 拡張機能の状態を保存する
普通なら拡張機能をインスタンス化するのは1度だけです。
だから「拡張機能が前に呼び出されたときの状態を残すにはどうしたらいいのか」という疑問が生じるのは当然です。
ExtensionContext
API の Store
インターフェイスはそのために使用します。
拡張機能が後から参照する値を登録するための API なのです。
メソッドレベルのスコープで Store
を使用する具体例として TimingExtension
を見てください。
テストメソッドを実行しているときに ExtensionContext
へ登録した値は、周辺の ExtensionContext
からは参照できないことに注意してください。
ExtensionContext
は入れ子構造にできるのですが、内側の可視性は制限されているのです。
値を登録したり参照したりするためのメソッドについては Store
の Javadoc を参照してください。
ExtensionContext.Store.CloseableResource CloseableResource のインスタンスに対して、登録した順序の逆順に close() メソッドを呼び出していきます。
|
5.14. 拡張機能で利用できるユーティリティ
junit-platform-commons
アーティファクトの提供する org.junit.platform.commons.support
パッケージには、アノテーションやクラスやリフレクションに関連する操作や、クラスパスを探索する機能に関する、保守されている 便利メソッドが含まれています。
テスト実行エンジンや拡張機能の作者は、JUnit Platform と一貫性のある操作を利用できるのです。
5.14.1. アノテーション
AnnotationSupport
は、アノテーションで修飾できる要素(パッケージ、アノテーション、クラス、インターフェイス、コンストラクタ、メソッド、フィールド)を操作する便利メソッドを提供します(static
メソッドです)。
アノテーションで修飾されているか、任意のアノテーションでメタアノテーションされているか、特定のアノテーションの探索、アノテーションで修飾されたメソッド、フィールド、クラス、インターフェイスの探索など。
クラスの実装したインターフェイスや、クラス階層を渡り歩いてアノテーションを探索するメソッドもあります。
詳しくは AnnotationSupport
の Javadoc を参照してください。
5.14.2. クラス
ClassSupport
は、クラス(java.lang.Class
のインスタンスなど)を操作する便利メソッドを提供します(static
メソッドです)。
詳しくは ClassSupport
の Javadoc を参照してください。
5.14.3. リフレクション
ReflectionSupport
は JDK の標準ライブラリが提供するリフレクション API やクラスロードの仕組みを拡張する便利メソッドを提供します(static
メソッドです)。
クラスパスから任意の述語式にマッチするクラスを探索する、指定したクラスをロードして新しいインスタンスを作成する、メソッドを探索して実行する(クラス階層を渡り歩くなど)、など。
詳しくは ReflectionSupport
の Javadoc を参照してください。
5.14.4. 修飾子(Modifier)
ModifierSupport
はクラスのメンバーやクラス自体の修飾子を操作する便利メソッドを提供します。
メンバーの可視性が public/private/abstract/static
のどれなのか、など。
詳しくは ModifierSupport
の Javadoc を参照してください。
5.15. ユーザーコードと拡張機能の相対的な実行順序
1つ以上のテストメソッドを持つテストクラスを実行するとき、ユーザーの記述したテストとライフサイクルメソッドに加えて、いくつもの拡張機能(コールバック)が呼び出されることになります。
テストの実行順序 も参照してください。 |
5.15.1. ユーザーコードと拡張機能の順序関係
次の図はユーザーコードと拡張機能の相対的な順序を示しています。 ユーザーの記述したテストとライフサイクルメソッドはオレンジ色、拡張機能が実装したコールバックは青色です。 灰色は1つのテストメソッドを実行するとき、および、全てのテストメソッドについて繰り返し実行するときの範囲を示しています。
次の表は ユーザーコードと拡張機能の順序関係 を実行する16の手順を詳しく記述したものです。
手順 | インターフェイス/アノテーション | 説明 |
---|---|---|
1 |
インターフェイス |
拡張機能:テストコンテナのあらゆるテストメソッドより先に実行する |
2 |
アノテーション |
ユーザーコード:テストコンテナのあらゆるテストメソッドより先に実行する |
3 |
インターフェイス |
拡張機能: |
4 |
インターフェイス |
拡張機能:テストコンテナのそれぞれのテストメソッドを実行する前に実行する |
5 |
アノテーション |
ユーザーコード:テストコンテナのそれぞれのテストメソッドを実行する前に実行する |
6 |
インターフェイス |
拡張機能: |
7 |
インターフェイス |
拡張機能:テストコンテナのそれぞれのテストメソッドを実行する直前に実行する |
8 |
アノテーション |
ユーザーコード:テストコンテナのそれぞれのテストメソッドを実行する |
9 |
インターフェイス |
拡張機能:テストメソッドがスローした例外を処理する例外処理ハンドラーを実行する |
10 |
インターフェイス |
拡張機能:テストコンテナのそれぞれのテストメソッドを実行した直後、あるいは、例外処理ハンドラーを実行した直後に実行する |
11 |
アノテーション |
ユーザーコード:テストコンテナのそれぞれのテストメソッドを実行した後に実行する |
12 |
インターフェイス |
拡張機能: |
13 |
インターフェイス |
拡張機能:テストコンテナのそれぞれのテストメソッドより後に実行する |
14 |
アノテーション |
ユーザーコード:テストコンテナの全てのテストメソッドより後に実行する |
15 |
インターフェイス |
拡張機能: |
16 |
インターフェイス |
拡張機能:テストコンテナの全てのテストメソッドより後に実行する |
単純な場合はテストメソッドを実行するだけでしょう(手順8)。 他の手順を実行するかどうかは、ユーザーコードや拡張機能が、対応するライフサイクルメソッドを提供しているかどうかによって決まります。 ライフサイクルメソッドのコールバックについては、対応するインターフェイスや拡張機能の Javadoc を参照してください。
全てのユーザーコード(メソッド)の呼び出しは、InvocationInterceptor
で割り込み可能です。
5.15.2. コールバックによる包み隠す振る舞い
JUnit Jupiter は、複数のライフサイクルコールバック(BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback
)を登録した場合、それぞれの呼び出しを 包み隠す ことを保証します。
例えば、Extension1
と Extension2
を順番に登録した場合、Extension1
の提供する全ての "before" コールバックは、Extension2
の提供する全ての "before" コールバックより 先に 実行することが保証されています。
同様に、Extension1
の提供する全ての "after" コールバックは、Extension2
の提供する全ての "after" コールバックより 後に 実行することが保証されています。
この関係を「Extension1
は Extension2
を 包み隠す」と呼んでいます。
ユーザーコードの提供する ライフサイクルメソッド (テストクラスとテストメソッド を参照)は、クラス階層やインターフェイス階層についても 包み隠す 関係になることが保証されています。
-
基底クラスから継承した
@BeforeAll
メソッドは 隠蔽 も オーバーライド もできません。 さらに、派生クラスの@BeforeAll
メソッドより 先に 実行します。-
同様に、インターフェイスから継承した
@BeforeAll
メソッドは 隠蔽 も オーバーライド もできません。 そして、実装クラスの@BeforeAll
メソッドより 先に 実行します。
-
-
基底クラスから継承した
@AfterAll
メソッドは 隠蔽 も オーバーライド もできません。 さらに、派生クラスの@AfterAll
メソッドより 後に 実行します。-
同様に、インターフェイスから継承した
@AfterAll
メソッドは 隠蔽 も オーバーライド もできません。 そして、実装クラスの@AfterAll
メソッドより 後に 実行します。
-
-
基底クラスから継承した
@BeforeEach
メソッドは オーバーライド できません。 さらに、派生クラスの@BeforeEach
メソッドより 先に 実行します。-
同様に、インターフェイスから継承した
@BeforeEach
メソッドは オーバーライド できません。 そして、実装クラスの@BeforeEach
メソッドより 先に 実行します。
-
-
基底クラスから継承した
@AfterEach
メソッドは オーバーライド できません。 さらに、派生クラスの@AfterEach
メソッドより 後に 実行します。-
同様に、インターフェイスから継承した
@AfterEach
メソッドは オーバーライド できません。 そして、実装クラスの@AfterEach
メソッドより 後に 実行します。
-
次のコード例はここまでに説明した振る舞いを示しています。
具体的になんらかの処理するようにはなっていませんが、テストがデータベースとやりとりする一般的なシナリオを再現しています。
Logger
クラスから static
インポートしているメソッドは、実行時の情報をログに出力するようになっているため、ユーザーコードと拡張機能のどちらで実装しているコールバックなのか分かりやすくなっています。
import static example.callbacks.Logger.afterEachCallback;
import static example.callbacks.Logger.beforeEachCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class Extension1 implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
beforeEachCallback(this);
}
@Override
public void afterEach(ExtensionContext context) {
afterEachCallback(this);
}
}
import static example.callbacks.Logger.afterEachCallback;
import static example.callbacks.Logger.beforeEachCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class Extension2 implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
beforeEachCallback(this);
}
@Override
public void afterEach(ExtensionContext context) {
afterEachCallback(this);
}
}
import static example.callbacks.Logger.afterAllMethod;
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeAllMethod;
import static example.callbacks.Logger.beforeEachMethod;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
/**
* Abstract base class for tests that use the database.
*/
abstract class AbstractDatabaseTests {
@BeforeAll
static void createDatabase() {
beforeAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".createDatabase()");
}
@BeforeEach
void connectToDatabase() {
beforeEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".connectToDatabase()");
}
@AfterEach
void disconnectFromDatabase() {
afterEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".disconnectFromDatabase()");
}
@AfterAll
static void destroyDatabase() {
afterAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".destroyDatabase()");
}
}
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeAllMethod;
import static example.callbacks.Logger.beforeEachMethod;
import static example.callbacks.Logger.testMethod;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Extension of {@link AbstractDatabaseTests} that inserts test data
* into the database (after the database connection has been opened)
* and deletes test data (before the database connection is closed).
*/
@ExtendWith({ Extension1.class, Extension2.class })
class DatabaseTestsDemo extends AbstractDatabaseTests {
@BeforeAll
static void beforeAll() {
beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".beforeAll()");
}
@BeforeEach
void insertTestDataIntoDatabase() {
beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
}
@Test
void testDatabaseFunctionality() {
testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
}
@AfterEach
void deleteTestDataFromDatabase() {
afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
}
@AfterAll
static void afterAll() {
beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".afterAll()");
}
}
DatabaseTestsDemo
を実行すると、次のように出力されるでしょう。
@BeforeAll AbstractDatabaseTests.createDatabase() @BeforeAll DatabaseTestsDemo.beforeAll() Extension1.beforeEach() Extension2.beforeEach() @BeforeEach AbstractDatabaseTests.connectToDatabase() @BeforeEach DatabaseTestsDemo.insertTestDataIntoDatabase() @Test DatabaseTestsDemo.testDatabaseFunctionality() @AfterEach DatabaseTestsDemo.deleteTestDataFromDatabase() @AfterEach AbstractDatabaseTests.disconnectFromDatabase() Extension2.afterEach() Extension1.afterEach() @BeforeAll DatabaseTestsDemo.afterAll() @AfterAll AbstractDatabaseTests.destroyDatabase()
次のシーケンス図は DatabaseTestDemo
を実行したとき、JupiterTestEngine
の内部で行われている処理を(大部分省略して)示したものです。
JUnit Jupiter では 単独の クラスやインターフェイスに宣言された複数のライフサイクルメソッドの実行順序を保証 しません。
一見するとアルファベット順に見えるかもしれませんが、正確にはそうではありません。
@Test
メソッドの実行順序と同じようになっているのです。
単独の クラスやインターフェイスに宣言した複数のライフサイクルメソッドの実行順序は、意図的に分かりにくくした決定論的アルゴリズムによって決まります。 したがって、再びテストスイートを実行しても、同じ登録順になることが保証されています。再現可能なビルド(プロセス)を保証するためです。 |
さらに、単独の クラスやインターフェイスに宣言した複数のライフサイクルメソッドが 包み隠す 関係になることを保証 しません。
次のコード例は、テストクラスに定義したライフサイクルメソッドの(間違った)実行順序に依存しているせいでいろいろ 壊れています。
-
データベース接続を
open()
する 前に テストデータをinsert
しています。おそらくデータベース接続エラーが発生するでしょう。 -
テストデータを
delete
する 前に データベース接続をclose()
しています。おそらくデータベース接続エラーが発生するでしょう。
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeEachMethod;
import static example.callbacks.Logger.testMethod;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Example of "broken" lifecycle method configuration.
*
* <p>Test data is inserted before the database connection has been opened.
*
* <p>Database connection is closed before deleting test data.
*/
@ExtendWith({ Extension1.class, Extension2.class })
class BrokenLifecycleMethodConfigDemo {
@BeforeEach
void connectToDatabase() {
beforeEachMethod(getClass().getSimpleName() + ".connectToDatabase()");
}
@BeforeEach
void insertTestDataIntoDatabase() {
beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
}
@Test
void testDatabaseFunctionality() {
testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
}
@AfterEach
void deleteTestDataFromDatabase() {
afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
}
@AfterEach
void disconnectFromDatabase() {
afterEachMethod(getClass().getSimpleName() + ".disconnectFromDatabase()");
}
}
BrokenLifecycleMethodConfigDemo
を実行すると、次のように出力されるでしょう。
Extension1.beforeEach() Extension2.beforeEach() @BeforeEach BrokenLifecycleMethodConfigDemo.insertTestDataIntoDatabase() @BeforeEach BrokenLifecycleMethodConfigDemo.connectToDatabase() @Test BrokenLifecycleMethodConfigDemo.testDatabaseFunctionality() @AfterEach BrokenLifecycleMethodConfigDemo.disconnectFromDatabase() @AfterEach BrokenLifecycleMethodConfigDemo.deleteTestDataFromDatabase() Extension2.afterEach() Extension1.afterEach()
次のシーケンス図は BrokenLifecycleMethodConfigDemo
を実行したとき、JupiterTestEngine
の内部で行われている処理を(大部分省略して)示したものです。
このセクションで説明した振る舞いが前提になるため、JUnit チームとしては、テストクラスやテストインターフェイスに定義するライフサイクルメソッド(テストクラスとテストメソッド を参照)は、それぞれの種類ごとに1つだけ定義するようにして、お互いに依存しないようにすることをお勧めします。 |
6. 高度な使い方
6.1. JUnit Platform Launcher API
JUnit 5 の特徴的な目標の1つとして、ビルドツールや IDE などのプログラムから JUnit を操作する強力かつ安定したインターフェイスを提供することが挙げられます。 目的は、テストを探索したり実行したりする内部動作と、外部から指定するテストをふるい落とす条件やその他の設定を完全に分離することです。
JUnit 5 では Launcher
という考え方を導入しました。
テストを探索し、選抜し、実行する要素です。
さらに、専用の TestEngine
を使用すれば、Spock や Cucumber や FitNesse のようなサードパーティライブラリを JUnit Platform で実行できます。
Launcher API は junit-platform-launcher
モジュールに含まれています。
実際に Launcher API を使用している好例は junit-platform-console
プロジェクトの ConsoleLauncher
です。
6.1.1. テストの発見
テストの発見 を JUnit Platform から独立した機能にすることで、旧バージョンの JUnit によるテストクラスやテストメソッドの特定に悩まされてきたビルドツールや IDE は楽になったはずです。
使い方。
import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.platform.engine.FilterResult;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryListener;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;
import org.junit.platform.launcher.PostDiscoveryFilter;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherConfig;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
import org.junit.platform.reporting.legacy.xml.LegacyXmlReportGeneratingListener;
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(
selectPackage("com.example.mytests"),
selectClass(MyTestClass.class)
)
.filters(
includeClassNamePatterns(".*Tests")
)
.build();
try (LauncherSession session = LauncherFactory.openSession()) {
TestPlan testPlan = session.getLauncher().discover(request);
// ... discover additional test plans or execute tests
}
クラスやメソッドを選択したり、パッケージの全てのクラスを選択したり、クラスパスやモジュールパスから全てのテストクラスを検索したりできるのです。 テストの発見は、存在する全てのテスト実行エンジンが行います。
発見した結果は、読み取り専用で階層的な木構造構造のテスト計画(TestPlan
)になります。
データ構造の要素は LauncherDiscoveryRequest
に適合したテスト実行エンジン、テストクラス、テストメソッドです。
API のクライアントは木を行き来して節を特定し、詳細情報を取得し、取得元(クラスやメソッド、ファイル中の位置など)の情報を取得します。
テスト計画に含まれる全ての節には 固有のID が割り当てられており、一部のテストやテストのグループを実行するために使用できます。
テストを発見している途中に発生したイベントから何らかの洞察を得るため、API のクライアントは1つ以上の LauncherDiscoveryListener
を登録できます(LauncherDiscoveryRequestBuilder
でインスタンスを作成します)。
初期設定では LauncherDiscoveryRequestBuilder
が「中断および失敗」リスナーを登録します。
テストの発見に失敗したら、ただちに発見処理全体を中断するためです。
設定パラメータの junit.platform.discovery.listener.default
に完全修飾クラス名を指定すれば変更できます。
6.1.2. テストの実行
テストを実行するには発見フェーズと同じ LauncherDiscoveryRequest
を使用するか、新しい要求を作成して使用します。
進行状況や結果を報告するには1つ以上の TestExecutionListener
を登録します。
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(
selectPackage("com.example.mytests"),
selectClass(MyTestClass.class)
)
.filters(
includeClassNamePatterns(".*Tests")
)
.build();
SummaryGeneratingListener listener = new SummaryGeneratingListener();
try (LauncherSession session = LauncherFactory.openSession()) {
Launcher launcher = session.getLauncher();
// Register a listener of your choice
launcher.registerTestExecutionListeners(listener);
// Discover tests and build a test plan
TestPlan testPlan = launcher.discover(request);
// Execute test plan
launcher.execute(testPlan);
// Alternatively, execute the request directly
launcher.execute(request);
}
TestExecutionSummary summary = listener.getSummary();
// Do something with the summary...
execute()
メソッドには返り値がありません。
代わりに TestExecutionListener
でテストの結果を集約できるようになっています。
具体的には SummaryGeneratingListener
や LegacyXmlReportGeneratingListener
や
UniqueIdTrackingListener
のソースコードを参照してください。
6.1.3. テスト実行エンジンの登録
JUnit は3種類の TestEngine
の実装クラスを提供しています。
-
junit-jupiter-engine
: JUnit Jupiter の基本となるエンジンです -
junit-vintage-engine
: 骨董品 のようなテストを JUnit Platform で実行するための、JUnit 4 を薄いレイヤーで包み込んだエンジンです -
junit-platform-suite-engine
: JUnit Platform で宣言的に構成したテストスイートを実行するエンジンです
サードパーティライブラリの作者は junit-platform-engine
モジュールの TestEngine
インターフェイスの実装クラスを提供し、登録 させることができます。
テスト実行エンジンは Java の ServiceLoader
で登録します。
例えば、junit-jupiter-engine
モジュールは org.junit.jupiter.engine.JupiterTestEngine
を登録するのですが、それは jar ファイルの /META-INF/services/org.junit.platform.engine.TestEngine
に org.junit.jupiter.engine.JupiterTestEngine
と記述されているからです。
HierarchicalTestEngine を抽象基底クラスとして使用すると、テストを発見するロジックだけを実装すれば済むので便利です(junit-jupiter-engine でも使用しています)。
HierarchicalTestEngine は Node インターフェイスを実装いた TestDescriptors を実装するようになっており、並列実行にも対応しています。
|
接頭辞が
junit- のテスト実行エンジン ID は JUnit チームのために予約済みですLauncher API は JUnit チームの公開する
|
6.1.4. テスト発見後処理フィルター(PostDiscoveryFilter) を登録する
Lancher API へ渡す LauncherDiscoveryRequest
にテスト発見後処理フィルターを設定するだけでなく、Java の ServiceLoader
を使用して PostDiscoveryFilter
の実装クラスを発見し、自動的に Launcher へ登録できます。
例えば、PostDiscoveryFilter
を実装した example.CustomTagFilter
(の完全修飾クラス名)を /META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter
に記述しておけば、自動的に登録するのです。
6.1.5. セッションリスナー(LauncherSessionListener)を登録する
LauncherSessionListener
の実装クラスを登録すると、LauncherSession
を開いたとき(Launcher
が最初にテストを発見し、実行する前)や、閉じるとき(それ以上発見したり実行したりするテストが無くなったとき)に通知します。
LauncherFactory
へ渡す LauncherConfig
で手続き的に登録することもできますし、Java の ServiceLoader
を使用して自動的に登録することもできます(自動登録機能が有効なら)。
LauncherSessionListener
は JVM が1回だけ setup
と teardown
を実行する状況に最適化されています。
1つのセッションで最初のテストを発見する前と、最後のテストが終了した後に呼び出されるからです。
セッションの有効範囲は使用している IDE やビルドツールによって異なりますが、基本的にはテストを実行する JVM のライフサイクルと一致します。
次のコード例は、最初のテストを発見する前に HTTP サーバーを起動し、最後のテストが終了した後に HTTP サーバーを停止するリスナーです。
package example.session;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.sun.net.httpserver.HttpServer;
import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;
public class GlobalSetupTeardownListener implements LauncherSessionListener {
private Fixture fixture;
@Override
public void launcherSessionOpened(LauncherSession session) {
// Avoid setup for test discovery by delaying it until tests are about to be executed
session.getLauncher().registerTestExecutionListeners(new TestExecutionListener() {
@Override
public void testPlanExecutionStarted(TestPlan testPlan) {
if (fixture == null) {
fixture = new Fixture();
fixture.setUp();
}
}
});
}
@Override
public void launcherSessionClosed(LauncherSession session) {
if (fixture != null) {
fixture.tearDown();
fixture = null;
}
}
static class Fixture {
private HttpServer server;
private ExecutorService executorService;
void setUp() {
try {
server = HttpServer.create(new InetSocketAddress(0), 0);
}
catch (IOException e) {
throw new UncheckedIOException("Failed to start HTTP server", e);
}
server.createContext("/test", exchange -> {
exchange.sendResponseHeaders(204, -1);
exchange.close();
});
executorService = Executors.newCachedThreadPool();
server.setExecutor(executorService);
server.start(); (1)
int port = server.getAddress().getPort();
System.setProperty("http.server.port", String.valueOf(port)); (2)
}
void tearDown() {
server.stop(0); (3)
executorService.shutdownNow();
}
}
}
1 | HTTP サーバーを起動します |
2 | テストから参照するための待ち受けポート番号を動的に決定し、システムプロパティへ設定します |
3 | HTTP サーバーを停止します |
このサンプルコードでは JDK に付属している jdk.httserver
モジュールの実装を使用していますが、他の実装でも同じように記述できるでしょう。
リスナーを自動的に JUnit Platform へ登録するには、次のようなリソースファイルをテストランタイムクラスパスに配置します(src/test/resources
などに配置することになるでしょう)。
example.session.GlobalSetupTeardownListener
テストは次のように記述できます。
package example.session;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.net.HttpURLConnection;
import java.net.URL;
import org.junit.jupiter.api.Test;
class HttpTests {
@Test
void respondsWith204() throws Exception {
String port = System.getProperty("http.server.port"); (1)
URL url = new URL("http://localhost:" + port + "/test");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode(); (2)
assertEquals(204, responseCode); (3)
}
}
1 | リスナーの設定したシステムプロパティから待ち受けポート番号を読み取ります |
2 | HTTP サーバーにリクエストを送信します |
3 | レスポンスのステータスコードを確認します |
6.1.6. テスト発見リスナー(LauncherDiscoveryListener)を登録します
Lancher API へ渡す LauncherDiscoveryRequest
にテスト発見後処理フィルターを設定するだけでなく、Java の ServiceLoader
を使用して LauncherDiscoveryListener
の実装クラスを自動的に発見し、LauncherFactory
で作成した Launcher
へ登録できます。
例えば、LauncherDiscoveryListener
を実装した example.CustomLauncherDiscoveryListener
(の完全修飾クラス名)を /META-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener
に記述しておけば、自動的に登録するのです。
6.1.7. テスト実行リスナー(TestExecutionListener)を登録する
Launcher
API のメソッドで手続き的にテスト実行リスナーを登録するだけでなく、Java の ServiceLoader
を使用して TestExecutionListener
の実装クラスを自動的に発見し、LauncherFactory
で作成した Launcher
へ登録できます。
例えば、TestExecutionListener
を実装した example.CustomTestExecutionListener
(の完全修飾クラス名)を /META-INF/services/org.junit.platform.launcher.TestExecutionListener
に記述しておけば、自動的に登録するのです。
6.1.8. テスト実行リスナーの設定
TestExecutionListener
を Launcher
API で手続き的に登録するときは、手続き的にリスナーの設定を変更できます。
例えばコンストラクタやセッターメソッドを使う方法です。
しかし、ServiceLoader
で自動的に登録するときは(テスト実行リスナー(TestExecutionListener)を登録する を参照)、直接的にリスナーの設定を変更する方法がありません。
そういう場合、TestExecutionListener
の作者はリスナーの設定を 設定パラメータ で指定できるようにするといいでしょう。
リスナーは testPlanExecutionStarted(TestPlan)
メソッドや testPlanExecutionFinished(TestPlan)
メソッドを実装すれば、TestPlan
から設定パラメータを参照できます。
具体例は UniqueIdTrackingListener
を参照してください。
6.1.9. テスト実行リスナーを不活性にする
テスト実行リスナーを 使わずに テストスイートを実行できるようになっていると役に立つ場合があります。
例えば、自作の TestExecutionListener
が外部システムにテスト結果を送信するようになっているとしたら、デバッグ実行した結果 は送信したくないはずです。
そういうときは、設定パラメータの junit.platform.execution.listeners.deactivate
へテスト実行リスナーの完全修飾クラス名を指定すると、そのテスト実行リスナーは不活性(未登録状態)になります。
不活性にできるのは また、テスト実行を開始する前にテスト実行リスナーを登録するようになっているので、 |
パターンマッチ記法
パターンマッチ記法 を参照してください。
6.1.10. Launcher の設定
自動検出機能や登録機能より、テストエンジンやリスナーの登録を詳細に制御したいときは、LauncherConfig
のインスタンスを作成して LauncherFactory
に渡すようにします。
普通は ビルダー API で LauncherConfig
のインスタンスを作成します。
LauncherConfig launcherConfig = LauncherConfig.builder()
.enableTestEngineAutoRegistration(false)
.enableLauncherSessionListenerAutoRegistration(false)
.enableLauncherDiscoveryListenerAutoRegistration(false)
.enablePostDiscoveryFilterAutoRegistration(false)
.enableTestExecutionListenerAutoRegistration(false)
.addTestEngines(new CustomTestEngine())
.addLauncherSessionListeners(new CustomLauncherSessionListener())
.addLauncherDiscoveryListeners(new CustomLauncherDiscoveryListener())
.addPostDiscoveryFilters(new CustomPostDiscoveryFilter())
.addTestExecutionListeners(new LegacyXmlReportGeneratingListener(reportsDir, out))
.addTestExecutionListeners(new CustomTestExecutionListener())
.build();
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectPackage("com.example.mytests"))
.build();
try (LauncherSession session = LauncherFactory.openSession(launcherConfig)) {
session.getLauncher().execute(request);
}
6.2. JUnit Platform レポート機能
junit-platform-reporting
アーティファクトには、テスト結果レポートを生成する TestExecutionListener
の実装クラスが複数含まれています。
これらのリスナーを使用するのはたいていの場合 IDE やビルドツールです。
org.junit.platform.reporting.legacy.xml
パッケージには、次のような実装クラスがあります。
-
LegacyXmlReportGeneratingListener
はTestPlan
のルートノードごとに XML 形式のテスト結果レポートを生成します。 生成するレポートは JUnit 4 のテスト結果レポート形式として事実上の標準になっている XML 形式のテスト結果レポートで、Ant により一般的になりました。LegacyXmlReportGeneratingListener
は コンソールランチャー から使うと便利です。
junit-platform-launcher モジュールにもテスト結果レポートの生成に使用できる TestExecutionListener の実装クラスが含まれています。
詳しくは リスナーを使用する を参照してください。
|
6.3. JUnit Platform テストスイートエンジン
JUnit Platform では、JUnit Platform に対応している あらゆる テスト実行エンジンで、宣言的なテストスイートの宣言と実行ができるようになっています。
6.3.1. 準備
junit-platform-suite-api
と junit-platform-suite-engine
に加えて、少なくとも1つ のテスト実行エンジンをクラスパスから参照できるようにしなければなりません。
具体的な情報(グループIDやアーティファクトIDやバージョンなど)は 依存ライブラリに関するメタデータ を参照してください。
6.3.2. テストスイートを宣言するアノテーション @Suite
の使用例
あるクラスを @Suite
で修飾すると、JUnit Platform はテストスイートとして認識します。
次のコード例では選択式(selector)とフィルターアノテーションを組み合わせてテストスイートの項目を構成しています。
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
@IncludeClassNamePatterns(".*Tests")
class SuiteDemo {
}
高度な設定項目
テストスイートに組み込むテストを発見し、選択するための設定項目は大量に存在します。
利用できるアノテーションと設定項目の完全な一覧は org.junit.platform.suite.api パッケージの Javadoc を参照してください。
|
6.4. JUnit Platform 検査キット
junit-platform-testkit
アーティファクトは、JUnit Platform でテスト計画を実行し、テスト結果と期待結果を検証するための機能を提供します。
ただし、JUnit Platform 1.4 から単独のテスト実行エンジン(テスト実行エンジン検査キット を参照)による実行しかできなくなりました。
6.4.1. テスト実行エンジン検査キット
org.junit.platform.testkit.engine
パッケージは、JUnit Platform で実行した TestEngine
のテスト結果を期待結果と検証するためのフルーエント API を提供します。
主なエントリポイントは EngineTestKit
クラスの static
メソッド engine()
と execute()
です。
LauncherDiscoveryRequest
を作成するフルーエント API の利点を活用して、柔軟に engine()
を選択することをお勧めします。
Launcher API の LauncherDiscoveryRequestBuilder で LauncherDiscoveryRequest を作成するなら、EngineTestKit の execute() メソッドを使用してください。
|
以降のコード例では次のテストクラスを使用します。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import example.util.Calculator;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(OrderAnnotation.class)
public class ExampleTestCase {
private final Calculator calculator = new Calculator();
@Test
@Disabled("for demonstration purposes")
@Order(1)
void skippedTest() {
// skipped ...
}
@Test
@Order(2)
void succeedingTest() {
assertEquals(42, calculator.multiply(6, 7));
}
@Test
@Order(3)
void abortedTest() {
assumeTrue("abc".contains("Z"), "abc does not contain Z");
// aborted ...
}
@Test
@Order(4)
void failingTest() {
// The following throws an ArithmeticException: "/ by zero"
calculator.divide(1, 0);
}
}
説明を簡略化するため、以降のセクションではエンジン ID が "junit-jupiter"
である JUnit 本体の JupiterTestEngnine
を使用します。
独自の TestEngine
を試したければ、エンジン ID を変更するか、EngineTestKit.engine(TestEngine)
メソッドを使ってください。
6.4.2. 統計情報を確認する
検査キットで最もよく使われる機能の1つが、TestPlan
を実行している間に発生したイベントの統計情報を確認する機能です。
次のコード例では、Junit Jupiter のテスト実行エンジンが処理した テストコンテナ と テスト に関する統計情報を確認しています。
利用できる統計情報について詳しくは EventStatistics
の Javadoc を参照してください。
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
class EngineTestKitStatisticsDemo {
@Test
void verifyJupiterContainerStats() {
EngineTestKit
.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.containerEvents() (4)
.assertStatistics(stats -> stats.started(2).succeeded(2)); (5)
}
@Test
void verifyJupiterTestStats() {
EngineTestKit
.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.testEvents() (6)
.assertStatistics(stats ->
stats.skipped(1).started(3).succeeded(1).aborted(1).failed(1)); (7)
}
}
1 | テスト実行エンジンを指定する |
2 | テストクラスとして ExampleTestCase を指定する |
3 | TestPlan を実行する |
4 | テストコンテナ のイベントを選択する |
5 | テストコンテナ のイベントに関する統計情報を確認する |
6 | テスト のイベントを選択する |
7 | テスト のイベントに関する統計情報を確認する |
verifyJupiterContainerStats() では、JupiterTestEngine と ExampleTestCase の両方が認識しているテストコンテナについて、started と succeeded イベントが 2 回発生していることを確認しています。
|
6.4.3. イベントを確認する
例えば、ExampleTestCase
の skippedTest()
をスキップした理由を確認するには、次のように記述します。
以降のコード例における 使用している 条件 について詳しくは |
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.test;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.junit.platform.testkit.engine.Events;
class EngineTestKitSkippedMethodDemo {
@Test
void verifyJupiterMethodWasSkipped() {
String methodName = "skippedTest";
Events testEvents = EngineTestKit (5)
.engine("junit-jupiter") (1)
.selectors(selectMethod(ExampleTestCase.class, methodName)) (2)
.execute() (3)
.testEvents(); (4)
testEvents.assertStatistics(stats -> stats.skipped(1)); (6)
testEvents.assertThatEvents() (7)
.haveExactly(1, event(test(methodName),
skippedWithReason("for demonstration purposes")));
}
}
1 | テスト実行エンジンを指定する |
2 | テストクラス ExampleTestCase の skippedTest() を指定する |
3 | TestPlan を実行する |
4 | テスト のイベントを選択する |
5 | テスト のイベントをローカル変数に保存する |
6 | ついでにイベントの統計情報を確認する |
7 | 記録した テスト イベントに、"for demonstration purposes" というメッセージを含むイベントが1つだけ存在し、skippedTest() メソッドをスキップしたことを確認する |
failingTest()
メソッドのスローした例外クラスを確認するときは、次のように記述します。
使用している 条件 について詳しくは |
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
class EngineTestKitFailedMethodDemo {
@Test
void verifyJupiterMethodFailed() {
EngineTestKit.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.testEvents() (4)
.assertThatEvents().haveExactly(1, (5)
event(test("failingTest"),
finishedWithFailure(
instanceOf(ArithmeticException.class), message("/ by zero"))));
}
}
1 | テスト実行エンジンを指定する |
2 | テストクラスとして ExampleTestCase を指定する |
3 | TestPlan を実行する |
4 | テスト のイベントを選択する |
5 | 記録した テスト イベントに、"/ by zero" というメッセージを含む ArithmeticException をスローしたイベントが1つだけ存在し、failingTest() メソッドが失敗したことを確認する |
普通なら不要ですが、TestPlan
を実行している間に発生した 全ての イベントを確認しなければならない場合があるかもしれません。
次のコード例では EngineTestKit
API の assertEventsMatchExactly()
メソッドでそのようなことをしています。
|
順序を無視して 部分的に 比較したければ、assertEventsMatchLooselyInOrder()
や assertEventsMatchLoosely()
を使うといいでしょう。
import static org.junit.jupiter.api.condition.JRE.JAVA_18;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.container;
import static org.junit.platform.testkit.engine.EventConditions.engine;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.started;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
import java.io.StringWriter;
import java.io.Writer;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnJre;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.opentest4j.TestAbortedException;
class EngineTestKitAllEventsDemo {
@Test
@DisabledOnJre(JAVA_18) // https://github.com/assertj/assertj-core/issues/2340
void verifyAllJupiterEvents() {
Writer writer = // create a java.io.Writer for debug output
EngineTestKit.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.allEvents() (4)
.debug(writer) (5)
.assertEventsMatchExactly( (6)
event(engine(), started()),
event(container(ExampleTestCase.class), started()),
event(test("skippedTest"), skippedWithReason("for demonstration purposes")),
event(test("succeedingTest"), started()),
event(test("succeedingTest"), finishedSuccessfully()),
event(test("abortedTest"), started()),
event(test("abortedTest"),
abortedWithReason(instanceOf(TestAbortedException.class),
message(m -> m.contains("abc does not contain Z")))),
event(test("failingTest"), started()),
event(test("failingTest"), finishedWithFailure(
instanceOf(ArithmeticException.class), message("/ by zero"))),
event(container(ExampleTestCase.class), finishedSuccessfully()),
event(engine(), finishedSuccessfully()));
}
}
1 | テスト実行エンジンを指定する |
2 | テストクラスとして ExampleTestCase を指定する |
3 | TestPlan を実行する |
4 | テスト のイベントを選択する |
5 | デバッグ用に全てのイベントを writer へ出力する(System.out や System.err のような OutputStream に出力することもできます) |
6 | テストエンジンの発生した 全ての イベントが想定した順序になっているか確認する |
前のコード例に登場する debug()
は次のような出力をするでしょう。
All Events:
Event [type = STARTED, testDescriptor = JupiterEngineDescriptor: [engine:junit-jupiter], timestamp = 2018-12-14T12:45:14.082280Z, payload = null]
Event [type = STARTED, testDescriptor = ClassTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase], timestamp = 2018-12-14T12:45:14.089339Z, payload = null]
Event [type = SKIPPED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:skippedTest()], timestamp = 2018-12-14T12:45:14.094314Z, payload = 'for demonstration purposes']
Event [type = STARTED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:succeedingTest()], timestamp = 2018-12-14T12:45:14.095182Z, payload = null]
Event [type = FINISHED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:succeedingTest()], timestamp = 2018-12-14T12:45:14.104922Z, payload = TestExecutionResult [status = SUCCESSFUL, throwable = null]]
Event [type = STARTED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:abortedTest()], timestamp = 2018-12-14T12:45:14.106121Z, payload = null]
Event [type = FINISHED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:abortedTest()], timestamp = 2018-12-14T12:45:14.109956Z, payload = TestExecutionResult [status = ABORTED, throwable = org.opentest4j.TestAbortedException: Assumption failed: abc does not contain Z]]
Event [type = STARTED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:failingTest()], timestamp = 2018-12-14T12:45:14.110680Z, payload = null]
Event [type = FINISHED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:failingTest()], timestamp = 2018-12-14T12:45:14.111217Z, payload = TestExecutionResult [status = FAILED, throwable = java.lang.ArithmeticException: / by zero]]
Event [type = FINISHED, testDescriptor = ClassTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase], timestamp = 2018-12-14T12:45:14.113731Z, payload = TestExecutionResult [status = SUCCESSFUL, throwable = null]]
Event [type = FINISHED, testDescriptor = JupiterEngineDescriptor: [engine:junit-jupiter], timestamp = 2018-12-14T12:45:14.113806Z, payload = TestExecutionResult [status = SUCCESSFUL, throwable = null]]
7. API の進化
JUnit 5 の主要な目標の1つとして、多数のプロジェクトで使われつつ継続的に JUnit を進化させるため、メンテナーにとっての利便性を向上することが挙げられます。 JUnit 4 では、拡張機能やツールの作者が利用できる部品は、最初から用意されていた内部部品に限られていたため、とても苦労させられただけでなく、やりたいことができない場合もありました。
JUnit 5 が全てのライフサイクルに関するインターフェイスやクラスやメソッドを公開しているのは、そういう理由です。
7.1. API のバージョンと状態
公開している全てのアーティファクトのバージョン番号の形式は <major>.<minor>.<patch>
です。
インターフェイス、クラス、メソッド、アノテーションにも、@API Guardian
の @API
で同じバージョン番号が指定されています。
status
属性には次のような値が指定されています。
状態 |
内容 |
|
内部用。JUnit 以外で使用するべきではない要素です。予告せずに削除する場合があります。 |
|
廃止予定。将来のマイナーアップデートで削除する可能性があるため、使用しないほうがいい要素です。 |
|
実験的。JUnit チームとしてフィードバックを募集している新しい実験的な機能に関連する要素です。
注意して使用してください。
将来的に |
|
保守。後方互換性を保つため、少なくとも 次のマイナーアップデート(同じメジャーバージョン)では変更しない要素です。
将来的に削除する場合は、まず状態を |
|
安定。後方互換性を保つため、同じメジャーバージョンの間は変更しない要素です。 |
インターフェイスやクラスが @API
で修飾されているなら、可視性が public
のメンバーについても同じ意味になります。
インターフェイスやクラスのメンバーには、インターフェイスやクラスより安定性の低い status
を指定する場合があります。
7.2. 実験的な API
次の表は現在のバージョンで 実験的 @API(status = EXPERIMENTAL)
になっている要素です。
注意して使用してください。
Package Name | Type Name | Since |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.3. 廃止予定の API
次の表は現在のバージョンで 廃止予定 @API(status = DEPRECATED)
になっている要素です。
次のリリースで削除される可能性があるため、できるだけ使用しないでください。
Package Name | Type Name | Since |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.4. ツール側の @API
対応
@API Guardian
プロジェクトでは、@API
で修飾した要素の供給者と消費者をツールから使用するための機能を提供する予定です。
例えば、ツールの使用している JUnit API の @API
をチェックできるようになるかもしれません。
8. Contributors
Browse the current list of contributors directly on GitHub.
9. Release Notes
The release notes are available here.
10. 付録
10.1. 再現性のあるビルド
JUnit 5.7 から、Javadoc を含まない jar ファイルを 再現性のあるビルド として作成するようになりました。
ビルド時の条件(Java のバージョンなど)が同じなら、何回ビルドをしてもバイト単位で同じ結果を生成するということです。
Maven Central や Sonartype に公開しているアーティファクトと同じビルド時の条件を用意できるなら、誰でも自分の環境で完全に同じアーティファクトをビルドできるのです。 Maven リポジトリのアーティファクトがソースコードからビルドした内容であることを確認できます。
10.2. 依存ライブラリに関するメタデータ
最終的なリリースやマイルストーンに対応するアーティファクトは Maven Central へデプロイします。 スナップショットのアーティファクトは SonarType の スナップショットリポジトリ の /org/junit へデプロイします。
10.2.1. JUnit Platform
-
Group ID:
org.junit.platform
-
Version:
1.9.0-SNAPSHOT
-
Artifact IDs:
junit-platform-commons
-
Common APIs and support utilities for the JUnit Platform. Any API annotated with
@API(status = INTERNAL)
is intended solely for usage within the JUnit framework itself. Any usage of internal APIs by external parties is not supported! junit-platform-console
-
Support for discovering and executing tests on the JUnit Platform from the console. See コンソールランチャー for details.
junit-platform-console-standalone
-
An executable JAR with all dependencies included is provided in Maven Central under the junit-platform-console-standalone directory. See コンソールランチャー for details.
junit-platform-engine
-
Public API for test engines. See テスト実行エンジンの登録 for details.
junit-platform-jfr
-
Provides a
LauncherDiscoveryListener
andTestExecutionListener
for Java Flight Recorder events on the JUnit Platform. See Java Flight Recorder を使用する for details. junit-platform-launcher
-
Public API for configuring and launching test plans — typically used by IDEs and build tools. See JUnit Platform Launcher API for details.
junit-platform-reporting
-
TestExecutionListener
implementations that generate test reports — typically used by IDEs and build tools. See JUnit Platform レポート機能 for details. junit-platform-runner
-
Runner for executing tests and test suites on the JUnit Platform in a JUnit 4 environment. See JUnit 4 で JUnit Platform を実行する for details.
junit-platform-suite
-
JUnit Platform Suite artifact that transitively pulls in dependencies on
junit-platform-suite-api
andjunit-platform-suite-engine
for simplified dependency management in build tools such as Gradle and Maven. junit-platform-suite-api
-
Annotations for configuring test suites on the JUnit Platform. Supported by the JUnit Platform Suite Engine and the JUnitPlatform runner.
junit-platform-suite-commons
-
Common support utilities for executing test suites on the JUnit Platform.
junit-platform-suite-engine
-
Engine that executes test suites on the JUnit Platform; only required at runtime. See JUnit Platform Suite Engine for details.
junit-platform-testkit
-
Provides support for executing a test plan for a given
TestEngine
and then accessing the results via a fluent API to verify the expected results.
10.2.2. JUnit Jupiter
-
Group ID:
org.junit.jupiter
-
Version:
5.9.0-SNAPSHOT
-
Artifact IDs:
junit-jupiter
-
JUnit Jupiter aggregator artifact that transitively pulls in dependencies on
junit-jupiter-api
,junit-jupiter-params
, andjunit-jupiter-engine
for simplified dependency management in build tools such as Gradle and Maven. junit-jupiter-api
-
JUnit Jupiter API for writing tests and extensions.
junit-jupiter-engine
-
JUnit Jupiter test engine implementation; only required at runtime.
junit-jupiter-params
-
Support for parameterized tests in JUnit Jupiter.
junit-jupiter-migrationsupport
-
Support for migrating from JUnit 4 to JUnit Jupiter; only required for support for JUnit 4’s
@Ignore
annotation and for running selected JUnit 4 rules.
10.2.3. JUnit Vintage
-
Group ID:
org.junit.vintage
-
Version:
5.9.0-SNAPSHOT
-
Artifact ID:
junit-vintage-engine
-
JUnit Vintage test engine implementation that allows one to run vintage JUnit tests on the JUnit Platform. Vintage tests include those written using JUnit 3 or JUnit 4 APIs or tests written using testing frameworks built on those APIs.