はじめに
株式会社Udzukiの@tsukakeiです。
東京大学大学院でソフトウェアテストの研究を行なっていました。
JUnit 5のユーザガイドを邦訳していきたいと思います。
JUnit 5 ユーザガイド 第1章 概要
JUnit 5 ユーザガイド 第2章 インストール
JUnit 5 ユーザガイド 第3章 テストを書く
JUnit 5 ユーザガイド 第4章 テストを実行する
JUnit 5 ユーザガイド 第5章 モデルの拡張
JUnit 5 ユーザガイド 第6章 JUnit4との違い
JUnit 5 ユーザガイド 第7章 先進的な話題
JUnit 5 ユーザガイド 第8章 APIの進化
全ての章を翻訳し、こちらにアップロードしました。
テストを書く
最初のテストケース
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class FirstJUnit5Tests {
@Test
void myFirstTest() {
assertEquals(2, 1 + 1);
}
}
アノテーション
JUnit Jupiterは、テストの設定やフレームワークの拡張のために次のアノテーションをサポートしています。
全てのコアなアノテーションは、junit-jupiter-api
モジュール内のorg.junit.jupiter.api
パッケージにあります。
アノテーション | 説明 |
---|---|
@Test |
テストメソッドであることを意味します。JUnit 4の@Test と異なり、このアノテーションはどのような属性も宣言しません。これは、JUnit Jupiterにおけるテスト拡張が専用のアノテーションを元に作動するからです。メソッドはオーバーライドされない限り、継承されます。 |
@ParameterizedTest |
パラメータ化テストであることを意味します。メソッドはオーバーライドされない限り、継承されます。 |
@RepeatedTest |
繰り返しテストのファクトリーメソッドであることを意味します。メソッドはオーバーライドされない限り、継承されます。 |
@TestFactory |
動的テストのファクトリーメソッドであることを意味します。メソッドはオーバーライドされない限り、継承されます。 |
@TestInstance |
テストインスタンス・ライフサイクルを設定するためにクラスに付与します。アノテーションは継承されます。 |
@TestTemplate |
テストケースのテンプレートメソッドであることを意味します。テンプレートは登録されたプロバイダによって返される呼び出しコンテキストの数に応じて複数回呼び出されます。 |
@DisplayName |
テストクラス、もしくはテストメソッドのカスタム表示名を意味します。アノテーションは継承されません。 |
@BeforeEach |
現在のクラス内にある各テスト(@Test , @RepeatedTest , @ParameterizedTest , @TestFactory )が実行される前(before)に実行されるメソッドを意味します。JUnit 4の@Before と類似したものです。メソッドはオーバーライドされない限り、継承されます。 |
@AfterEach |
現在のクラス内にある各テスト(@Test , @RepeatedTest , @ParameterizedTest , @TestFactory )が実行された後(after)に実行されるメソッドを意味します。JUnit 4の@After と類似したものです。メソッドはオーバーライドされない限り、継承されます。 |
@BeforeAll |
現在のクラス内にある全テスト(@Test , @RepeatedTest , @ParameterizedTest , @TestFactory )が実行される前(before)に実行されるメソッドを意味します。JUnit 4の@BeforeClass と類似したものです。メソッドは隠されるかオーバーライドされない限り、継承され、static でないといけません("クラスごと"テストインスタンス・ライフサイクルを使わない限り)。 |
@AfterAll |
現在のクラス内にある全テスト(@Test , @RepeatedTest , @ParameterizedTest , @TestFactory )が実行された後(after)に実行されるメソッドを意味します。JUnit 4の@AfterClass と類似したものです。メソッドは隠されるかオーバーライドされない限り、継承され、static でないといけません("クラスごと"テストインスタンス・ライフサイクルを使わない限り)。 |
@Nested |
ネストされたnon-staticなテストクラスであることを意味します。@BeforeAll と@AfterAll メソッドは、"クラスごと"テストインスタンス・ライフサイクルを使わない限り、@Nested クラスの中では直接使うことができません。 |
@Tag |
テストをクラスレベル、もしくはメソッドレベルでフィルタリングするためのタグを宣言することができます。TestNGのtest groups、もしくはJUnit 4のCategoriesと類似したものです。アノテーションはクラスレベルでは継承されますが、メソッドレベルでは継承されません。 |
@Disabled |
テストクラス、もしくはテストメソッドを無効化 することができます。JUnit 4の@Ignore と類似したものです。アノテーションは継承されません。 |
@ExtendWith |
カスタム拡張を登録することができます。アノテーションは継承されます。 |
次のアノテーションをつけたメソッドは値を返してはいけません。
(@Test
, @TestTemplate
, @RepeatedTest
, @BeforeAll
, @AfterAll
, @BeforeEach
, @AfterEach
)
注意点:いくつかのアノテーションは現在 experimentalにあるかもしれません。詳細に関しては、Experimental APIsをご覧ください。
メタアノテーションとアノテーションの組み合わせ
JUnit Jupiterアノテーションはメタアノテーションとして使うことができます。それはつまり、メタアノテーションの意味を自動で継承する独自の組み合わせアノテーションを定義することができるということです。
例えば、コードベースに@Tag("fast")
をコピー&ペーストする代わりに、あなたは@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 {
}
標準的なテストクラス
標準的なテストケース
import static org.junit.jupiter.api.Assertions.fail;
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
}
@AfterEach
void tearDown() {
}
@AfterAll
static void tearDownAll() {
}
}
情報:テストクラスもテストメソッドも
public
である必要はありません。
表示名
テストクラスとテストメソッドはカスタムされた表示名(スペース、特殊文字、さらには絵文字だって使えます。)を宣言することができます。それらがテストランナーとテストレポートによって表示されます。
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() {
}
}
アサーション
JUnit Jupiterには、JUnit 4が持っていたアサーションメソッドの多くを持っています。また、いくつかはJava 8のラムダ式で使うことができます。全てのJUnit Jupiterアサーションは、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 org.junit.jupiter.api.Test;
class AssertionsDemo {
@Test
void standardAssertions() {
assertEquals(2, 2);
assertEquals(4, 4, "The optional assertion 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 any
// failures will be reported together.
assertAll("person",
() -> assertEquals("John", 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("n"))
);
},
() -> {
// 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() {
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("a message");
});
assertEquals("a message", 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.
Thread.sleep(100);
});
}
private static String greeting() {
return "Hello, World!";
}
}
また、JUnit JupiterのいくつかのアサーションメソッドはKotlinで使うことができます。全てのJUnit Jupiter Kotlinアサーションは、org.junit.jupiter.api
パッケージのトップレベル関数です。
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.assertThrows
class AssertionsKotlinDemo {
@Test
fun `grouped assertions`() {
assertAll("person",
{ assertEquals("John", person.firstName) },
{ assertEquals("Doe", person.lastName) }
)
}
@Test
fun `exception testing`() {
val exception = assertThrows<IllegalArgumentException> ("Should throw an exception") {
throw IllegalArgumentException("a message")
}
assertEquals("a message", exception.message)
}
@Test
fun `assertions from a stream`() {
assertAll(
"people with name starting with J",
people
.stream()
.map {
// This mapping returns Stream<() -> Unit>
{ assertTrue(it.firstName.startsWith("J")) }
}
)
}
@Test
fun `assertions from a collection`() {
assertAll(
"people with last name of Doe",
people.map { { assertEquals("Doe", it.lastName) } }
)
}
}
サードパーティによるアサーションライブラリ
JUnit Jupiterによって提供されているアサーション機能は、多くのテストシナリオには十分なものだと思いますが、matchersのような、より強力で追加的な機能が求められたり、必要であることがあります。そのような場合、JUnitチームは、AssertJ、Hamcrest、Truthなどといったサードパーティによるアサーションライブラリの使用をお薦めします。したがって、開発者は自由に選んだアサーションライブラリを使うことができます。
例えば、matchersと流暢なAPI (fluent API) の組み合わせは、アサーションをよりわかりやすく、読みやすくするために使うことができます。しかしながら、JUnit Jupiterのorg.junit.jupiter.Assertions
クラスは、HamcrestのMatcher
を受け入れていたJUnit 4のorg.junit.jupiter.Assert
クラスのassertThat()
メソッドのようなものを提供していません。代わりに、開発者はサードパーティによるアサーションライブラリによって提供されているマッチャー用のサポートを使うことが奨励されています。
次の例は、JUnit Jupiterテストにおいて、どのようにしてHamcrestからのassertThat()
サポートを使うかを説明しています。Hamcrestライブラリがクラスパスに加えられている限り、assertThat()
やis()
、equalTo()
といったメソッドをstaticにインポートすることができます。また、それらをテストの中で、下に示すassertWithHamcrestMatcher()
のように使うことができます。
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import org.junit.jupiter.api.Test;
class HamcrestAssertionDemo {
@Test
void assertWithHamcrestMatcher() {
assertThat(2 + 1, is(equalTo(3)));
}
}
当然、JUnit 4のプログラミングモデルに基づいたレガシーコードもorg.junit.Assert#assertThat
を使い続けることができます。
アサンプション
JUnit Jupiterには、JUnit 4が持っていたアサンプションメソッドの多くを持っています。また、いくつかはJava 8のラムダ式で使うことができます。全てのJUnit Jupiterアサンプションは、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 org.junit.jupiter.api.Test;
class AssumptionsDemo {
@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, 2);
});
// perform these assertions in all environments
assertEquals("a string", "a string");
}
}
テストの無効化
テストクラス全体、もしくは各テストメソッドは@Disabled
アノテーションか、条件付きテスト実行で話されているアノテーションの一つ、もしくはカスタム実行条件によって無効化することができます。
これは@Disabled
テストクラスです。
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
そして、これは@Disabled
テストメソッドを含むテストクラスです。
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class DisabledTestsDemo {
@Disabled
@Test
void testWillBeSkipped() {
}
@Test
void testWillBeExecuted() {
}
}
条件付きテスト実行
JUnit Jupiterの実行条件拡張APIによって、開発者は、コンテナ、またはある条件に基づいたテストをプログラム的に可能にしたり不可能することができます。そのような条件の最も単純な例は、@Disabled
をサポートしているビルトインのDisabledCondition
です(テストを不可能にするをご覧ください)。@Disabled
に加えて、JUnit Jupiterは、org.junit.jupiter.api.condition
パッケージ内の他のいくつかのアノテーションベースの条件もサポートしています。org.junit.jupiter.api.condition
パッケージによって、開発者はコンテナやテストを宣言的に可能にしたり不可能にすることができます。詳細については、次のセクションをご覧ください。
組み合わせアノテーションのヒント:次のセクションで挙げられている条件付きアノテーションはどれも、組み合わせアノテーションを作るためのメタアノテーションとして使えるかもしれない。例えば、@EnabledOnOsのデモにある
@TestOnMac
アノテーションは、どのようにして@Test
と@EnableOnOs
を一つの再利用できるアノテーションで結合できるかを示しています。
次のセクションで挙げられている条件付きアノテーションはそれぞれ、テストインターフェイス、テストクラス、またはテストメソッドに一度だけ宣言することができます。もし条件付きアノテーションが直接的か間接的、またはメタ的に、ある要素に複数存在していた場合、JUnitによって発見された初めのアノテーションが使われます;他の追加的なアノテーションは無視されます。しかしながら、
org.junit.jupiter.api.condition
パッケージでは、各条件付きアノテーションは他の条件付きアノテーションと共に使われているかもしれません。
オペレーティングシステムの条件
@EnabledOnOs
と@DisabledOnOs
を使うことで、特定のオペーティングシステム上でコンテナかテストを、有効にしたり無効にすることができます。
@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 {
}
Java実行時条件
@EnabledOnJre
と@DisabledOnJre
を使うことで、特定のバージョンのJava実行環境(JRE)でコンテナかテストを、有効にしたり無効にすることができます。
@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
// ...
}
@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
// ...
}
@Test
@DisabledOnJre(JAVA_9)
void notOnJava9() {
// ...
}
システムプロパティ条件
@EnabledIfSystemProperty
と@DisabledIfSystemProperty
を使うことで、named
で指定したJVMシステムプロパティの値に応じてコンテナかテストを、有効にしたり無効にすることができます。matches
属性を使うことで、値は正規表現として解釈されます。
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
// ...
}
@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
// ...
}
環境変数条件
@EnabledIfEnvironmentVariable
と@DisabledIfEnvironmentVariable
を使うことで、named
で指定したOSの環境変数の値に応じてコンテナかテストを、有効にしたり無効にすることができます。matches
属性を使うことで、値は正規表現として解釈されます。
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
// ...
}
@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {
// ...
}
スクリプトベースの条件
JUnit Jupiterは、@EnabledIf
と@DisabledIf
を使うことで、設定されたスクリプトの評価値に応じてコンテナかテストを、有効にしたり無効にすることができる機能を提供しています。スクリプトは、JavaScriptかGroovy、またはJSR 223で定義されたJava Scripting APIをサポートしているスクリプト言語であれば何でも、書くことができます。
@EnabledIf
と@DisabledIf
を使った条件付きテスト実行は、現在実験的な機能です。詳細については、Experimental APIsをご覧ください。
もしスクリプトのロジックが、今使っているOSか、JREのバージョン、またはあるJVMシステムプロパティや環境変数に依存している場合、その目的に合ったビルトインアノテーションを使うといいかもしれません。詳細については、この章の前のセクションをご覧ください。
もし同じスクリプトベースの条件を多数使っている場合、より速く、型安全で、メンテナンスのしやすい条件を実装するために、それに合った実行条件拡張を書くことを考えてみてください。
@Test // Static JavaScript expression.
@EnabledIf("2 * 3 == 6")
void willBeExecuted() {
// ...
}
@RepeatedTest(10) // Dynamic JavaScript expression.
@DisabledIf("Math.random() < 0.314159")
void mightNotBeExecuted() {
// ...
}
@Test // Regular expression testing bound system property.
@DisabledIf("/32/.test(systemProperty.get('os.arch'))")
void disabledOn32BitArchitectures() {
assertFalse(System.getProperty("os.arch").contains("32"));
}
@Test
@EnabledIf("'CI' == systemEnvironment.get('ENV')")
void onlyOnCiServer() {
assertTrue("CI".equals(System.getenv("ENV")));
}
@Test // Multi-line script, custom engine name and custom reason.
@EnabledIf(value = {
"load('nashorn:mozilla_compat.js')",
"importPackage(java.time)",
"",
"var today = LocalDate.now()",
"var tomorrow = today.plusDays(1)",
"tomorrow.isAfter(today)"
},
engine = "nashorn",
reason = "Self-fulfilling: {result}")
void theDayAfterTomorrow() {
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
assertTrue(tomorrow.isAfter(today));
}
スクリプトバインディング
次の名前は、各スクリプト条件とバインドされています。そのため、スクリプトで使用可能です。accessorは、単純な String get(String name)
メソッドを通して、マップライクな構造へのアクセスを提供します。
Name | Type | Description |
---|---|---|
systemEnvironment | accessor | OS環境変数アクセサ |
systemProperty | accessor | JVMシステムプロパティアクセサ |
junitConfigurationParameter | accessor | Configurationパラメータアクセサ |
junitDisplayName | String | テストかコンテナの表示名 |
junitTags | Set<String> | テストかコンテナに振られている全てのタグ |
junitUniqueId | String | テストかコンテナのユニークなID |
タグとフィルタリング
テストクラスとメソッドは@Tag
を用いてタグ付けすることができます。それらのタグは、後でテスト発見と実行をフィルタリングするのに使われます。
タグのシンタックスルール
- タグは
null
か空であってはならない。 - トリミングされたタグは空白文字を含んではならない。
- トリミングされたタグはISO制御文字を含んではならない。
-
トリミングされたタグは次の予約語のいずれも含んではならない。
-
,
:カンマ -
(
:左カッコ -
)
:右カッコ -
&
:アンパサンド -
|
:縦棒 -
!
:エクスクラメーション
-
上の文章で、トリミングされたというのは、語頭と語尾の空白文字を取り除いたということを意味します。
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("fast")
@Tag("model")
class TaggingDemo {
@Test
@Tag("taxes")
void testingTaxCalculation() {
}
}
テストインスタンス・ライフサイクル
各テストメソッドの独立した実行と、変化可能なテストインスタンスの状態による予期せぬ副作用を避けるため、JUnitは各テストメソッドを実行する前に、各テストクラスの新しいインスタンスを生成します(何がテストメソッドなのかは、下の注意書きをご覧ください)。この"メソッドごと"のテストインスタンス・ライフサイクルはJUnit Jupiterではデフォルトの動作で、以前の全てのバージョンのJUnitと類似したものになっています。
@Disabled
や@DisabledOnOs
といった条件によって無効化されたテストメソッドであっても、テストクラスはインスタンス化されることに注意してください。これは例えば、"クラスごと"テストインスタンス・ライフサイクルが有効である時でも同様です。
もしJUnit Jupiterに全テストメソッドを同じテストインスタンスで実行してほしい場合は、単にテストクラスに@TestInstance(Lifecycle.PER_CLASS)
アノテーションを付与するだけで実現可能です。このモードを使用する場合、新しいテストインスタンスは一度だけ生成されます。これによって、もしテストメソッドがインスタンス変数に保存された状態に依存する場合は、@BeforeEach
かAfterEach
メソッドによって状態をリセットする必要があるかもしれません。
”クラスごと”モードは、デフォルトの"メソッドごと"モードに比べていくつかの付加的な便益があります。特に"クラスごと"モードを使うと、インターフェイスのdefault
メソッドと同様に、@BeforeAll
と@AfterAll
メソッドをnon-staticメソッドとして宣言することが可能になります。そのため、"クラスごと"モードでは、@Nested
テストクラス内で@BeforeAll
と@AfterAll
メソッドを使うことができます。
Kotlinでテストを書いている場合、”クラスごと”テストインスタンス・ライフサイクルモードに切り替えることで、@BeforeAll
と@AfterAll
メソッドの実装がより容易になるかもしれません。
テストインスタンス・ライフサイクルの文脈内におけるテストメソッドは、
@Test
、@RerpeatedTest
、@ParameterizedTest
、@TestFactory
、@TestTemplate
が付与されたメソッド全てのことを意味します。
デフォルトのテストインスタンス・ライフサイクルを変更する
テストクラスかテストインターフェイスに@TestInstance
が付与されていない場合、JUnit Jupiterはデフォルトのライフサイクルモードを使います。
標準的なデフォルトモードはPER_METHOD
です;しかしながら、テスト計画全体のデフォルトを変更することが可能です。デフォルトのテストインスタンス・ライフサイクルを変更するには、単にjunit.jupiter.testinstance.lifecycle.default
設定パラメータに、TestInstance.Lifecycle
で定義されたenum定数の値を、大文字・小文字を無視してセットするだけです。これは、JVMシステムプロパティとして渡すか、Launcher
に渡されるLauncherDiscoveryRequest
内の設定パラメータとして渡すか、JUnitプラットフォーム設定ファイル(詳細については、Configuration Parametersをご覧ください。)を通して渡します。
例えば、デフォルトのテストインスタンス・ライフサイクルをLifeCycle.PER_CLASS
に設定するには、JVMを次のシステムプロパティで起動することです。
-Djunit.jupiter.testinstance.lifecycle.default=per_class
しかしながら、JUnitプラットフォーム設定ファイルを通してデフォルトのテストインスタンス・ライフサイクルを設定する場合は、次の内容を含んだjunit-platform.properties
という名前のファイルをクラスパスのルート(例えば、src/test/resources
)に生成する必要があることに注意してください。
junit.jupiter.testinstance.lifecycle.default = per_class
デフォルトのテストインスタンス・ライフサイクルを変更することは、一貫性を持って適用しないと、予測不可能な結果と壊れやすいビルドを導く恐れがあります。例えば、デフォルトとして”クラスごと”をビルド設定していながら、IDE上で"メソッドごと"でテスト実行されてしまった場合、ビルドサーバに表れるエラーをデバッグすることは困難になる恐れがあります。そのため、JVMシステムプロパティの代わりに、JUnitプラットフォーム設定ファイルを使ってデフォルトモードを変更することをお薦めします。
ネストされたテスト
ネストされたテストは、テスト開発者が様々なグループのテスト間の関係を表現することを可能にします。これがその例です。
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());
}
}
}
}
non-staticなネストされたクラス(つまり、インナークラス)のみが
@Nested
テストクラスとなります。ネストは任意に深くすることができ、それらのインナークラスは一つの例外を除いて、テストクラスの完全なメンバーとして考えられます。例外とは、@BeforeAll
と@AfterAll
がデフォルトでは作動しないことです。その理由は、Javaがインナークラスにstatic
なメンバーを許さないためです。しかしながら、この制限は@Nested
テストクラスに@TestInstance(Lifecycle.PER_CLASS)
を付与することで回避することができます(テストインスタンス・ライフサイクルをご覧ください)。
コンストラクタとメソッドへの依存性注入
全てのJUnitの全バージョンでは、テストコンストラクタかメソッドは、パラメータを持つことが許されていませんでした(少なくとも標準的なRunner
実装の下では)。JUnit Jupiterでの大きな変化の一つとして、テストコンストラクタとテストメソッドのどちらもパラメータを持つことが許されたことがあります。このことは、大きな柔軟性をもたらし、コンストラクタとメソッドに依存性を注入することが可能になりました。
ParameterResolver
は、実行時に動的にパラメータを解決することを望むテスト拡張のためのAPIを定義しています。もしテストコンストラクタか、@Test
、@TestFactory
、@BeforeEach
、@AfterEach
、@BeforeAll
、@AfterAll
メソッドがパラメータを許容する時は、パラメータは登録されたParameterResolver
によって実行時に解決されないといけません。
現在は、3つのビルトイン・リゾルバが自動的に登録されます。
-
TestInfoParameterResolver
:もしメソッドパラメータがTestInfo
型であった場合、TestInfoParameterResolver
は、現在のテストに応じたTestInfo
のインスタンスをパラメータの値として供給します。TestInfo
は、テストの表示名、テストクラス、テストメソッド、付いているタグ名といった現在のテストに関する情報を集めるのに使うことができます。表示名は、テストクラス名かテストメソッド名といった技術的な名前か、@DisplayedName
で設定されたカスタム名のどちらかです。TestInfo
は、JUnit 4のTestName
の代替のようなものです。次のコードは、テストコンストラクタ、@BeforeEach
、@Test
メソッドにどのように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
の繰り返しの総回数に関する情報を集めるのに使うことができます。しかしながら、RepetitionInfoParameterResolver
は、@RepeatedTest
以外の文脈以外では登録されていないことに注意してください。繰り返しテストの例をご覧ください。 -
TestReporterParameterResolver
:もしメソッドパラメータがTestReporter
型であった場合、TestReporterParameterResolver
はTestReporter
のインスタンスを供給します。TestReporter
は、現在のテスト実行に関する付加的な情報を公開することに使うことができます。データは、TestExecutionListener.reportingEntryPublished()
を通して消費され、それによってIDEから閲覧できたりレポートに含まれます。JUnit Jupiterでは、JUnit 4でstdout
やstderr
を使って情報を出力していた箇所にTestReporter
を使うことができます。@RunWith(JUnitPlatform.class)
を使うと、全てのレポートされたエントリをstdout
に出力します。
import java.util.HashMap;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestReporter;
class TestReporterDemo {
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
@Test
void reportSeveralValues(TestReporter testReporter) {
HashMap<String, String> values = new HashMap<>();
values.put("user name", "dk38");
values.put("award year", "1974");
testReporter.publishEntry(values);
}
}
他のパラメータリゾルバは、
@ExtendedWith
を用いた適切な拡張を登録することによって明示的に有効化する必要があります。
カスタムParameterResolver
の例のために、MockitoExtension
を確認しましょう。リリース可能なものではありませんが、拡張モデルとパラメータ解決プロセス両方の単純さと表現性を例示しています。MyMockitoTest
は、どのように@BeforeEach
と@Test
メソッド内にMockitoモックを注入するかを示しています。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import com.example.Person;
import com.example.mockito.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class MyMockitoTest {
@BeforeEach
void init(@Mock Person person) {
when(person.getName()).thenReturn("Dilbert");
}
@Test
void simpleTestWithInjectedMock(@Mock Person person) {
assertEquals("Dilbert", person.getName());
}
}
テストインターフェイスとデフォルトメソッド
JUnit Jupiterは、@Test
、@RepeatedTest
、@ParameterizedTest
、@TestFactory
、@TestTemplate
、@BeforeEach
、@AfterEach
にインターフェイスデフォルトメソッドを宣言できるようにしています。@BeforeAll
と@AfrterAll
はテストインターフェイス内でstatic
メソッドを宣言するか、もしテストクラスに@TestInstance(Lifecycle.PER_CLASS)
が付与されている場合はインターフェイスデフォルトメソッドを宣言することができます(テストインスタンス・ライフサイクルをご覧ください)。いくつかの例を示します。
@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {
static final Logger LOG = Logger.getLogger(TestLifecycleLogger.class.getName());
@BeforeAll
default void beforeAllTests() {
LOG.info("Before all tests");
}
@AfterAll
default void afterAllTests() {
LOG.info("After all tests");
}
@BeforeEach
default void beforeEachTest(TestInfo testInfo) {
LOG.info(() -> String.format("About to execute [%s]",
testInfo.getDisplayName()));
}
@AfterEach
default void afterEachTest(TestInfo testInfo) {
LOG.info(() -> String.format("Finished executing [%s]",
testInfo.getDisplayName()));
}
}
interface TestInterfaceDynamicTestsDemo {
@TestFactory
default Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test in test interface", () -> assertTrue(true)),
dynamicTest("2nd dynamic test in test interface", () -> assertEquals(4, 2 * 2))
);
}
}
@ExtenedWoth
と @Tag
はテストインターフェイスとして宣言することができるため、インターフェイスを実装したクラスは自動的にタグと拡張を継承します。TimingExtension
のソースコードを見るには、BeforeとAfterのテスト実行コールバック
をご覧ください。
@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}
テストクラスでは、これらのテストインターフェイスを実装することで適用することができます。
class TestInterfaceDemo implements TestLifecycleLogger,
TimeExecutionLogger, TestInterfaceDynamicTestsDemo {
@Test
void isEqualValue() {
assertEquals(1, 1, "is always equal");
}
}
TestInterfaceDemo
の実行結果の出力は、次のものと似たようなものになります。
:junitPlatformTest
INFO example.TestLifecycleLogger - Before all tests
INFO example.TestLifecycleLogger - About to execute [dynamicTestsFromCollection()]
INFO example.TimingExtension - Method [dynamicTestsFromCollection] took 13 ms.
INFO example.TestLifecycleLogger - Finished executing [dynamicTestsFromCollection()]
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
Test run finished after 190 ms
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 3 containers successful ]
[ 0 containers failed ]
[ 3 tests found ]
[ 0 tests skipped ]
[ 3 tests started ]
[ 0 tests aborted ]
[ 3 tests successful ]
[ 0 tests failed ]
BUILD SUCCESSFUL
この機能の他のあり得る適用としては、インターフェイス契約のためにテストを書くことです。例えば、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 returnsPositiveNumberComparedToSmallerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(value.compareTo(smallerValue) > 0);
}
@Test
default void returnsNegativeNumberComparedToSmallerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(smallerValue.compareTo(value) < 0);
}
}
テストクラスは、両方のインターフェイスを実装することができ、それによって対応するテストを継承します。もちろん、抽象メソッドを実装する必要があります。
class StringTests implements ComparableContract<String>, EqualsContract<String> {
@Override
public String createValue() {
return "foo";
}
@Override
public String createSmallerValue() {
return "bar"; // 'b' < 'f' in "foo"
}
@Override
public String createNotEqualValue() {
return "baz";
}
}
上記のテストは、単に例であって、完全ではありません。
繰り返しテスト
JUnit Jupiterは、@RepeatedTest
を付与し、繰り返してほしい回数を設定するだけで、特定回数テストを繰り返す機能を提供しています。繰り返しテストの各呼び出しは、通常の@Test
メソッドの実行のように振る舞い、全く同じライフサイクル・コールバックと拡張をサポートしています。
次の例は、どのようにして自動で10回繰り返すrepeatedTes()
の宣言するかを示しています。
@RepeatedTest(10)
void repeatedTest() {
// ...
}
繰り返し回数の設定に加えて、@RepeatedTest
のname
属性を用いることで、カスタム表示名も設定することができます。さらに、表示名は、静的テキストと動的プレースホルダの組み合わせのパターンにすることもできます。次のプレースホルダが現在サポートされています。
-
{displayName}
:@RepeatedTest
メソッドの表示名 -
{currentRepetition}
: 現在の繰り返し回数 -
{totalRepetition}
: 繰り返し回数の合計
ある繰り返し回数時点でのデフォルトの表示名は、次のパターンに基づいて生成されます:"repetition {currentRepetition} of {totalRepetitions}"
。そのため、先ほどの例の各繰り返し回数における表示名は次のようになります:repetition 1 of 10
、repetition 2 of 10
、などなど。もし@RepeatedTest
の表示名を、各繰り返し回数での表示名に含めたい場合は、独自のカスタムパターンを定義するか、事前定義されたRepeatedTest.LONG_DISPLAY_NAME
パターンを使うことができます。後者は、"{displayName} :: repetition {currentRepetition} of {totalRepetitions}"
と等しいもので、結果はrepeatedTest() :: repetition 1 of 10
、repeatedTest() :: repetition 2 of 10
となります。
現在の繰り返し回数と繰り返しの合計数の情報をプログラムから集めるためには、開発者は@RepeatedTest
か@BeforeEach
、もしくは@AfterEach
にRepetitionInfo
インスタンスを注入することができます。
繰り返しテストの例
このセクションの最後にあるRepeatedTestsDemo
クラスは、繰り返しテストのいくつかの例を示しています。
repeatedTest()
メソッドは、前のセクションからの例です。一方、repeatedTestWithRepetitionInfo()
は、繰り返しの合計数と現在の繰り返し回数を得るために、どのようにしてRepetitionInfo
インスタンスをテストに注入すればいいかを示しています。
その次の2つのメソッドは、どのようにして@RepeatedTest
のカスタム@DisplayName
を各繰り返しの表示名内に含ませるかを示しています。customDisplayName()
は、カスタム表示名とカスタムパターンを組み合わせており、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(testInfo.getDisplayName(), "Repeat! 1/1");
}
@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1");
}
@RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
void repeatedTestInGerman() {
// ...
}
}
ConsoleLauncher
か、unicodeテーマを有効化したjunitPlatformTest
Gradleプラグインを使うと、RepeatedTestsDemo
の実行結果は次のようなコンソール出力を行います。
├─ 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... :: 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 ✔
パラメータ化テスト
パラメータ化テストを使うと、テストを異なる引数で複数回実行できるようになります。パラメータ化テストは、通常の@Test
の代わりに@ParameterizedTest
を付与するだけで宣言することができます。さらに、各呼び出して供給され、テストで消費される引数として、最低でも1つのsourceを宣言する必要があります。
次の例は、パラメータ化テストを示していて、@ValueSource
を使ってString
配列を引数のソースに設定しています。
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(isPalindrome(candidate));
}
上記のパラメータ化テストメソッドを実行すると、各呼び出しは別々に報告されます。例えば、ConsoleLauncher
は次のようなものを出力します。
palindromes(String) ✔
├─ [1] racecar ✔
├─ [2] radar ✔
└─ [3] able was I ere I saw elba ✔
パラメータ化テストは、現在実験的な機能です。詳細については、Experimental APIsをご覧ください。
必要なセットアップ
パラメータ化テストを使うためには、junit-jupiter-params
を依存関係に加える必要があります。詳細については、依存性のメタデータをご覧ください。
引数の消費
典型的なパラメータ化テストメソッドは、引数ソースインデックスとメソッドパラメータインデックス間の1対1相関(@CsvSource
の例をご覧ください)に従って、設定されたソース(引数のソースをご覧ください。)から直接、引数を消費します。しかしながら、パラメータ化テストメソッドは、ソースから得た引数をひとつのオブジェクトに集約して、メソッドに渡すこともできます(引数集約をご覧ください)。付加的な引数もまた、ParameterResolver
によって提供されます(例えば、TestInfo
やTestReporter
のインスタンスなど)。特に、パラメータ化テストメソッドは、次のルールに従って形式的なパラメータを宣言する必要があります。
- 0個以上のインデックスされた引数が最初に宣言されないといけない。
- 0個以上の集約器が次に宣言されないといけない。
- 0個以上の
ParameterResolver
によって供給される引数が最後に宣言されないといけない。
この文脈で、インデックスされた引数というのは、ArgumentsProvider
によって提供されるArguments
内で与えられたインデックスに対応する引数です。インデックスされた引数は、引数として、メソッドの形式パラメータリストと同じインデックスの箇所に渡されます。集約器は、ArgumentsAccessor
型の全てのパラメータか、@AggregateWith
の付与された全てのパラメータです。
引数のソース
すぐに使えるように、JUnit Jupiterはかなりの数のソースアノテーションを提供しています。次の各セクションはそれぞれ、簡潔な概要とそれぞれの例を提供しています。追加的な情報に関しては、org.junit.jupiter.params.provider
パッケージのJavaDocを参照してください。
@ValueSouce
@ValueSource
は最も単純なソースの一つです。リテラル値の配列を1つ設定することができ、パラメータ化テスト呼び出しにつき、1つのパラメータを提供することができます。
次のリテラル値の型が@ValueSource
にサポートされています。
short
byte
int
long
float
double
char
java.lang.String
java.lang.Class
例えば、次の@ParameterizedTest
メソッドはそれぞれ1
、2
、3
の値とともに3回呼び出されます。
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
assertTrue(argument > 0 && argument < 4);
}
@EnumSource
@EnumSource
は、Enum
定数に対して便利な機能を提供します。@EnumSource
は、使われる定数を特定するために、任意のnames
パラメータを提供します。もし指定されていない場合は、次の例のように全ての定数が使われます。
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithEnumSource(TimeUnit timeUnit) {
assertNotNull(timeUnit);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(TimeUnit timeUnit) {
assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}
@EnumSource
はまた、テストメソッドに渡すパラメータを細かく制御するために、任意のmode
パラメータを提供します。例えば、次の例では、enum定数プールからnamesを取り除いたり、正規表現を設定しています。
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" })
void testWithEnumSourceExclude(TimeUnit timeUnit) {
assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
assertTrue(timeUnit.name().length() > 5);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$")
void testWithEnumSourceRegex(TimeUnit timeUnit) {
String name = timeUnit.name();
assertTrue(name.startsWith("M") || name.startsWith("N"));
assertTrue(name.endsWith("SECONDS"));
}
@MethodSource
@MethodSource
では、テストクラスか外部クラスのファクトリーメソッドを1つ以上使うことができます。ファクトリーメソッドは、Stream
、Iterable
、Iterator
、または引き通の配列を返す必要があります。さらに、ファクトリーメソッドは、引数を受けてはいけません。テストクラス内のファクトリーメソッドは、テストクラスに@TestInstance(Lifecycle.PER_CLASS)
が付与されていない限り、static
である必要があります;一方、外部クラスのファクトリーメソッドは常にstatic
である必要があります。
パラメータが1つだけ必要な場合は、次の例が示しているように、パラメータの型のインスタンスのStream
を返すことができます。
@ParameterizedTest
@MethodSource("stringProvider")
void testWithSimpleMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("foo", "bar");
}
@MethodSource
を通して明示的にファクトリーメソッドの名前を提供しない場合、JUnit Jupiterは、慣習で現在の@ParameterizedTest
と同じ名前を持つファクトリーメソッドを探します。これを次の例で示します。
@ParameterizedTest
@MethodSource
void testWithSimpleMethodSourceHavingNoValue(String argument) {
assertNotNull(argument);
}
static Stream<String> testWithSimpleMethodSourceHavingNoValue() {
return Stream.of("foo", "bar");
}
DoubleStream
、IntStream
、LongStream
といったプリミティブ型のStreamもまた、次の例のようにサポートされています。
@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
assertNotEquals(9, argument);
}
static IntStream range() {
return IntStream.range(0, 20).skip(10);
}
テストメソッドが複数のパラメータを宣言している場合、下に示すようにArguments
のコレクションかストリームを返す必要があります。Arguments.of(Object...)
は、Arguments
インターフェイスで定義されているstatic
なファクトリーメソッドです。
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(3, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
Arguments.of("foo", 1, Arrays.asList("a", "b")),
Arguments.of("bar", 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#blankStrings")
void testWithExternalMethodSource(String blankString) {
// test with blank string
}
}
class StringsProviders {
static Stream<String> blankStrings() {
return Stream.of("", " ", " \n ");
}
}
@CsvSource
@CsvSource
は、引数リストをCSVとして表現できるようにします(つまり、String
リテラルです)。
@ParameterizedTest
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCsvSource(String first, int second) {
assertNotNull(first);
assertNotEquals(0, second);
}
@CsvSource
は、シングルクォーテーション'
を引用文字として使います。上の例と下の表の'baz, qux'
の値をご覧ください。引用された空の値''
は、空のString
となります;一方、完全に空の値はnull
参照として解釈されます。null
参照のターゲット値がプリミティブ型の場合、ArgumentConversionException
が投げられます。
Example Input | Resulting Argument List |
---|---|
@CsvSource({ "foo, bar" }) |
"foo" , "bar"
|
@CsvSource({ "foo, 'baz, qux'" }) |
"foo" , "baz, qux"
|
@CsvSource({ "foo, ''" }) |
"foo" , ""
|
@CsvSource({ "foo, " }) |
"foo" , null
|
@CsvFileSource
@CsvFileSource
は、CSVファイルをクラスパスから使えるようにします。CSVファイルの各行がパラメータテストの1呼び出しに相当しています。
@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSource(String first, int second) {
assertNotNull(first);
assertNotEquals(0, second);
}
Country, reference
Sweden, 1
Poland, 2
"United States of America", 3
@CsvSource
で使われているシンタックスとは対照的に、@CsvFileSource
では引用文字としてダブルクォーテーション"
を使います。上記の例の"United States of America"
をご覧ください。引用された空の値””
は、空のString
となります;一方、完全に空の値はnull
参照として解釈されます。null
参照のターゲット値がプリミティブ型の場合、ArgumentConversionException
が投げられます。
@ArgumentSource
@ArgumentSource
はカスタムの再利用可能なArgumentsProvider
を特定するために使うことができます。
@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("foo", "bar").map(Arguments::of);
}
}
引数変換
広げる変換
JUnit Jupiterは@ParamterizedTest
に供給する引数のために、広げるプリミティブ変換をサポートしています。例えば、@ValueSource(ints = { 1, 2, 3 })
が付与されたパラメータ化テストは、int
型のみならず、long
、float
、double
型の引数も受けることができます。
暗示的な変換
@CsvSource
のようなユースケースをサポートするために、JUnit Jupiterは多くの暗示的なビルトイン型変換を提供しています。変換プロセスは、各メソッドパラメータの宣言された型に依存します。
例えば、もし@ParameterizedTest
がTimeUnit
型のパラメータを宣言していて、ソースから供給された実際の型がString
であった場合、String
は自動的に対応するTimeUnit
enum定数に変換されます。
@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(TimeUnit argument) {
assertNotNull(argument.name());
}
String
インスタンスは、現在次のターゲット型に暗示的に変換されます。
ターゲット型 | 例 |
---|---|
boolean /Boolean
|
"true" → true
|
byte /Byte
|
"1" → (byte) 1
|
char /Character
|
"o" → 'o'
|
short /Short
|
"1" → (short) 1
|
int /Integer
|
"1" → 1
|
long /Long
|
"1" → 1L
|
float /Float
|
"1" → 1.0f
|
double /Double
|
"1" → 1.0d
|
Enum サブクラス |
"SECONDS" → TimeUnit.SECONDS
|
java.io.File |
"/path/to/file" → new File("path/to/file")
|
java.math.BigDecimal |
"123.456e789" → new BigDecimal("123.456e789")
|
java.math.BigInteger |
"1234567890123456789" → new BigInteger("1234567890123456789")
|
java.net.URI |
"http://junit.org/" → URI.create("http://junit.org/")
|
java.net.URL |
"http://junit.org/" → new URL("http://junit.org/")
|
java.nio.file.Path |
"/path/to/file" → Paths.get("/path/to/file")
|
java.time.Instant |
"1970-01-01T00:00:00Z" → Instant.ofEpochMilli(0)
|
java.time.LocalDateTime |
"2017-03-14T12:34:56.789" → LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)
|
java.time.LocalDate |
"2017-03-14" → LocalDate.of(2017, 3, 14)
|
java.time.LocalTime |
"12:34:56.789" → LocalTime.of(12, 34, 56, 789_000_000)
|
java.time.OffsetDateTime |
"2017-03-14T12:34:56.789Z" → OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
|
java.time.OffsetTime |
"12:34:56.789Z" → OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)
|
java.time.YearMonth |
"2017-03" → YearMonth.of(2017, 3)
|
java.time.Year |
"2017" → Year.of(2017)
|
java.time.ZonedDateTime |
"2017-03-14T12:34:56.789Z" → ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
|
java.util.Currency |
"JPY" → Currency.getInstance("JPY")
|
java.util.Locale |
"en" → new Locale("en")
|
java.util.UUID |
"d043e930-7b3b-48e3-bdbe-5a3ccfb833db" → UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db")
|
String-to-Object変換のフォールバック
Stringから上のリストで挙げたターゲット型への暗示的な変換に加えて、JUnit Jupiterでは、String
をある型への自動変換に対するフォールバック機能を提供します。それは目標とする、ターゲット型が、下の定義にまさに正確に合致したファクトリーメソッドかファクトリーコンストラクタを宣言するときです。
-
ファクトリーメソッド:ターゲットタイプの中で宣言されているnon-private、かつ
static
なメソッドで、1つのString
引数を取り、ターゲット型のインスタンスを返すもの。メソッド名は任意であり、いかなる慣習にも従う必要はありません。 -
ファクトリーコンストラクタ:ターゲット型のnon-privateなコンストラクタで、1つの
String
引数を取るもの。
もし複数のファクトリーメソッドが見つかった場合、それらは無視されます。もしファクトリーメソッドとファクトリーコンストラクタが見つかった場合、ファクトリーメソッドが、コンストラクタの代わりに使われます。
例えば、次の@ParameterizedTest
メソッドの中で、引数Book
はBook.fromTitle(String)
ファクトリーメソッドが呼び出されることで生成され、"42 Cats"
が本のタイトルとして渡されます。
@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
を特定することができます。
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithExplicitArgumentConversion(
@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(TimeUnit.valueOf(argument));
}
public class ToStringArgumentConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
assertEquals(String.class, targetType, "Can only convert to String");
return String.valueOf(source);
}
}
明示的な引数変換は、テストと拡張の著者らによって実装される必要があります。そのため、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());
}
引数集約
デフォルトでは、@ParameterizedTest
メソッドに渡される各引数
は、1つのメソッドパラメータに対応しています。その結果として、大量の引数を供給することが期待される引数ソースは、巨大なメソッドシグネチャーを導くことがあり得ます。
そのような場合、ArgumentAccessor
は、複数のパラメータの代わりに使うことができます。このAPIを使うことで、テストメソッドに渡された1つのパラメータを通して提供された引数にアクセスすることができます。さらに、暗黙的な変換
で述べた型変換もサポートしています。
@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
を用いた@ParameterizedTest
メソッドの引数への直接アクセスとは別に、JUnit Jupiterはカスタムで再利用可能な集約器の使用もサポートしています。
カスタム集約器を使うためには、単にArgumentAggregator
インターフェイスを実装し、@ParameterizedTest
メソッド内で互換可能なパラメータに対して、@AggregateWith
を付与して登録するだけです。集約の結果は、パラメータ化テストが呼び出された時に、対応するパラメータへの引数として提供されます。
@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
のようなカスタム結合アノテーションを作ることができます。次の例は、カスタム@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 {
}
表示名のカスタマイズ
デフォルトでは、パラメータ化テスト呼び出しの表示名は、呼び出しインデックスと呼び出しに対する全ての引数のString
表現を含んでいます。しかしながら、次の例のように@ParameterizedTest
アノテーションのname
属性によって呼び出し表示名をカスタマイズすることができます。
@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> first=''{0}'', second={1}")
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCustomDisplayNames(String first, int second) {
}
上記のメソッドをConsoleLauncher
を使って実行すると、次のような出力が表示されます。
Display name of container ✔
├─ 1 ==> first='foo', second=1 ✔
├─ 2 ==> first='bar', second=2 ✔
└─ 3 ==> first='baz, qux', second=3 ✔
カスタム表示名では、次のプレースホルダがサポートされています。
プレースホルダ | 説明 |
---|---|
{index} | 現在の呼び出しインデックス(1始まり) |
{arguments} | 完全な引数リスト(CSV形式) |
{0}, {1}, ... | 各引数 |
ライフサイクルと相互運用性
パラメータ化テストの各呼び出しは、通常の@Test
メソッドと同じライフサイクルを持っています。例えば、各呼び出し前には@BeforeEach
メソッドが実行されます。動的テスト
と同じように、呼び出しはIDEのテストツリーでは一つ一つ表れます。同一のテストクラスに、自由に@Test
と@ParameterizedTest
を混ぜることができます。
@ParameterizedTest
メソッドと合わせてParameterResolver
を使うことができます。しかしながら、引数ソースによって解決されたパラメータは、引数リストの最初に来る必要があります。テストクラスは様々なパラメータリストを持つパラメータ化テストと同様に通常のテストクラスを含んでいることもあるので、引数ソースからの値は、@BeforeEach
といったライフサイクル・メソッドやテストクラスコンストラクタは解決されません。
@BeforeEach
void beforeEach(TestInfo testInfo) {
// ...
}
@ParameterizedTest
@ValueSource(strings = "foo")
void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
testReporter.publishEntry("argument", argument);
}
@AfterEach
void afterEach(TestInfo testInfo) {
// ...
}
テストテンプレート
@TestTemplate
メソッドは、通常のテストケースではなく、むしろテストケースのためのテンプレートです。したがって、@TestTemplate
は、登録された提供器によって返される呼び出し文脈の数に応じて複数回呼び出されるものとして設計されています。そのため、登録されたTestTemplateInvocationContextProvider
拡張と併せて使われる必要があります。テストテンプレートメソッドの各呼び出しは、通常の@Test
メソッドの実行と同じように振る舞い、全く同じライフサイクル・コールバックと拡張が完全にサポートされています。用法例については、テストテンプレートに対する呼び出し文脈の提供を参照ください。
動的テスト
アノテーションで説明したJUnit Jupiterの@Test
アノテーションは、JUnit 4の@Test
アノテーションに非常に似通っています。どちらもテストケースを実装したメソッドを描写します。これらのテストケースはコンパイル時に完全に決定するという意味では静的であり、それらの振る舞いは実行時に変更することはできません。アサンプションは、意図的にかなり表現性に制限のあるものですが、動的な振る舞いの基本的な形式を提供します。
これらの標準的なテストに加えて、全く新しい種類のテストプログラミング・モデルがJUnit Jupiterでは導入されました。この新しいテストとは、動的テストです。動的テストは、@TestFactory
が付与されたファクトリーメソッドによって、実行時に生成されます。
@Test
メソッドとは対照的に、@TestFactory
メソッド自身はテストケースではなく、むしろテストケースのためのファクトリーです。そのため、動的テストはファクトリーの産出物となります。技術的なことを言うと、@TestFactory
メソッドは、DynamicNode
インスタンスのStream
、Collection
、Iterable
、もしくはIterator
を返さなければなりません。DynamicNode
のインスタンス化可能なサブクラスはDynamicContainer
とDynamicTest
です。DynamicContainer
インスタンスは、表示名と動的子ノードリストで構成されており、動的ノードの任意なネスト階層を生成します。DynamicTest
インスタンスは、遅れて実行され、テストケースを動的で非決定的に生成します。
@TestFactory
によって返されるStream
はどれも、stream.close()
を呼ぶことによって適切に閉じられます。これによって、Files.lines()
のような資源を安全に使うことができます。
@Test
メソッドと同様に、@TestFactory
メソッドもprivate
やstatic
でいることはできず、任意にParameterResolvers
で解決されるであろうパラメータを宣言することができます。
DynamicTest
は実行時に生成されるテストケースで、表示名と
Executableで構成されています。
Executableは
@FunctionalInterface`で、このインターフェイスは、ラムダ表現とメソッド参照として提供されることができる動的テストの実装であることを意味します。
動的テスト・ライフサイクル:動的テストの実行ライフサイクルは、通常の
@Test
ケースとは全く異なります。特に、各動的テストに対してのライフサイクル・コールバックはありません。このことは、@BeforeEach
と@AfterEach
、それに対応した拡張コールバックは@TestFactory
メソッドに対して実行され、各動的テストには実行されないことを意味します。つまり、動的テストに対してラムダ表現でテストインスタンスからフィールドにアクセスしても、それらのフィールドは、同じ@TestFactory
メソッドで生成された動的テストの実行中は、コールバックメソッドやその拡張によってリセットされません。
JUnit Jupiter 5.2.0-M1に関しては、動的テストは常にファクトリーメソッドによって生成される必要があります;しかしながら、これは後のリリースの登録機能によって補完されるかもしれません。
動的テストは現在実験的な機能です。詳細に関しては、Experimental APIsをご覧ください。
動的テストの例
次のDynamicTestsDemo
クラスは、テストファクトリーと動的テストのいくつかの例を示しています。
最初のメソッドは不正な型を返しています。不正な返却型はコンパイル時に検出することができないため、実行時に検出されJUnitException
が投げられます。
次の5つもメソッドは、DynamicTest
インスタンスのCollection
、Iterable
、Iterator
、Stream
を生成する非常に単純な例です。これらの例のほとんどは、実際に動的振る舞いを示しておらず、単に原則的にサポートされている返却型を示しています。しかしながら、dynamicTestsFromStream()
とdynamicTestsFromIntStream()
は、与えられたString
のセットと入力された数の範囲に対する動的テストの生成が、いかに簡単かを示しています。
次のメソッドは、性質上、真に動的なものです。generateRandomNumberOfTests()
は、ランダム数を生成するIterator
と表示名生成器、テスト実行器を実装しており、その3つをDynamicTest.stream()
に提供しています。generateRandomNumberOfTests()
の非決定的な振る舞いは、もちろんテスト反復可能性に抵触しており、注意深く取り扱われるべきではありますが、動的テストの表現性と力を示しています。
最後のメソッドは、DynamicContainer
を使って動的テストのネスト階層を生成しています。
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 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 org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
class DynamicTestsDemo {
// 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(true)),
dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
);
}
@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(
dynamicTest("3rd dynamic test", () -> assertTrue(true)),
dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))
);
}
@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(
dynamicTest("5th dynamic test", () -> assertTrue(true)),
dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))
).iterator();
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("A", "B", "C")
.map(str -> dynamicTest("test" + str, () -> { /* ... */ }));
}
@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> generateRandomNumberOfTests() {
// 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<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()))
))
)));
}
}