初Qiitaです。
2017/06/24に開催されたKANJAVA PARTY 2017 !!!にてJUnit5の味見と言うタイトルで発表させていただきました。
発表時点のバージョンはM4でしたが、M5でそこそこ変わってるところがありますのでフォローさせていただきます。
但し、ここで記載させていただくのは主にJUnit Jupiterについてですので、その他については原典のJUnit5のUser Guideを参照して下さいね。
全てのjarファイルに「Automatic-Module-Name」と言うManifest属性が付与されました
Jigsaw対応ですね。Jigsawはそんなに詳しくないので詳細は避けます。
@ParameterizedTest
のパラメタはテストメソッドに対してのみ適用されるようになりました
ちょっとわかりにくいですね。コードを書きます。
JUnit5M4では、setup1のメソッドコールバック時に引数があるため、@ParameterrizedTest
のパラメタを適用しようとするのですが、型が合わないので落ちます。その代わりsetup2のようにライフサイクルコールバックメソッドでParameterizedTestのパラメタを受けることができました。
JUnit5M5では、@ParameterizedTest
のパラメタはライフサイクルコールバックメソッドには適用されなくなりますので、逆にsetup2のコールバック時にエラーが発生します。
この修正は、@ParameterizedTest
と通常の@Test
が1つのテストクラスで混在したときに変な問題が起こらなくするための修正となります。
public class ParameterizedNGTest {
@BeforeEach
void setUp1(TestInfo info) {
}
@BeforeEach
void setUp2(String p1, String p2) {
System.out.println(p1 + p2);
}
@ParameterizedTest
@CsvSource({ "x, 1", "y, 2","z, 3" })
void testIt(String input, String expected) {
}
}
artifact名「junit-jupiter-migration-support」が「junit-jupiter-migrationsupport」に変更されました
MavenやGradleで取得する際に注意して下さい。
以下のAPIが変更されました
クラスが変わらない物はクラス名を省略しています。
旧API | 新API | 備考 |
---|---|---|
ParameterResolver#supports() | supportsParameter() | |
ParameterResolver#resolve() | resolveParameter() | |
ContainerExecutionCondition#evaluate() | ExecutionCondition#evaluateExecutionCondition() | ContainerExecutionConditionは廃止 |
TestExecutionCondition#evaluate() | ExecutionCondition#evaluateExecutionCondition() | TestExecutionConditionは廃止 |
TestExtensionContext#getTestException() | ExtensionContext#getExecutionException() | TestExtensionContextは廃止 |
TestExtensionContext#getTestInstance() | ExtensionContext#getTestInstance() | TestExtensionContextは廃止。戻り値もObject -> Optional<Object>へ変更 |
TestTemplateInvocationContextProvider#supports() | supportsTestTemplate() | |
TestTemplateInvocationContextProvider#resolve() | provideTestTemplateInvocationContexts() | |
ArgumentsProvider#arguments() | provideArguments() | |
ObjectArrayArguments#create() | Arguments#of() | ObjectArrayArgumentsは廃止 |
@MethodSource#names |
value |
@TestInstance
が導入されました
@TestInstance
アノテーションは、テストクラスのライフサイクルを制御するためのアノテーションです。
これまでのJUnitは、テストメソッド実行毎にテストクラスを実体化していました。
@RunWith(JUnitPlatform.class)
public class TestInstancePerMethodTest {
private int i = 0;
@Test
void test1(){System.out.println(i++);}
@Test
void test2(){System.out.println(i++);}
}
0
0
@TestInstance
を付与(かつLifecycle.PER_CLASSを指定)すると、テストクラスの実体化は一度しか行われなくなり結果が異なります。
@RunWith(JUnitPlatform.class)
@TestInstance(Lifecycle.PER_CLASS)
public class TestInstancePerClassTest {
private int i = 0;
@Test
void test1(){System.out.println(i++);}
@Test
void test2(){System.out.println(i++);}
}
0
1
@TestInstance
を付与することでテストクラスの実行コストは下がりますが、初期化処理は初期化時ではなく、@BeforeEach
で行わなければならなくなる点注意が必要です。
併せて、話が若干ややこしくなるのは、@BeforeAll
と@AfterAll
はテストクラスの実体化と合う形で指定しなければならない点も注意が必要です。
すなわち、PER_CLASSを指定するケースでは、静的メソッドでは無く動的メソッドに対して@BeforeAll
や@AfterAll
をアノテートしないと実行時エラーとなってしまいます。
また、@TestInstance
と@Nested
を併用すると、ネストされたテストクラスに対しても@BeforeAll
と@AfterAll
を記述できるようになりますので、適切に使い分けると良いことがあるかもしれませんね。
ついでに、@TestInstance
を利用した場合は、ライフサイクルコールバックの順序が変わります。
CallBackOrderTest.javaをそのまま実行した場合の出力結果は①のようになりますが、@TestInstance
をアノテートした場合は②のようになります。
面倒くせぇなぁとも思いますが、普通に考えりゃぁまぁそりゃそうかという結果です。
ExecutionCondition:false
beforeAll
postProcessTestInstance
ExecutionCondition:true
beforeEach
beforeTestExecution
foo
handleTestExecutionException
afterTestExecution
afterEach
afterAll
postProcessTestInstance
ExecutionCondition:false
beforeAll
ExecutionCondition:true
beforeEach
beforeTestExecution
foo
handleTestExecutionException
afterTestExecution
afterEach
afterAll
assertAllの仕様が変わりました
JUnit5M4までは、assertAll無いの個々のExecutable内で例外が発生した場合は、以降のExecutableは評価せずにテストをエラーとして扱っておりましたが、M5以降はブラックリスト例外以外の例外が発生しても評価を中断しないようになりました。
以下のようなコードがあった場合のM4とM5の実行結果を以下に示します。
@RunWith(JUnitPlatform.class)
public class AssertAllInExceptionTest {
@Test
void thrownNullPointerException() {
assertAll(
() -> {throw new NullPointerException();},
() -> assertEquals(2, 3)
);
}
}
M4の実行結果 | M5の実行結果 |
---|---|
![]() |
![]() |
M4はErrorとして、M5はFailureとしてかつ2つ目のExecutableが評価されていることが分かると思います。
後、先ほど「ブラックリスト例外」と言う言葉を使いましたが「それってなんやねん!」と言う疑問があります。
User Guideにブラックリスト例外(blacklisted exception)と言う言葉は今回のassertAllの仕様変更の告知の箇所と後1箇所M2のRelease Noteに記載がありました。
If the exception is a blacklisted exception such as an OutOfMemoryError, however, it will be rethrown.
「such as(など)ってなんやねん!!他に何があんねん!!」と言う問いの答えはUserGuideにはなくてコードにありました。
BlacklistedExceptions.javaと言うコードを見る限り現状OutOfMemoryErrorだけのようです。
@TestTemplate
が明文化されました
JUnit5M4からあったっぽいですが、User Guide には書いてなかったので見落としてました。
JUnit5M5のUser Guideには記載がありましたので、概要をここに書いておきます。
@TestTemplate
は@ParameterizedTest
と同様、テストコードとテストデータを分離する為の物だと思います。
コードを読んで分かったのですが、@ParameterizedTest
は@TestTemplate
をアノテートしているアノテーションなので、@TestTemplate
は@ParameterizedTest
に不満が無いなら気にしなくてもいいものかと思います。
以下は、User Guideを機械翻訳した物を若干手直しした物です。
3.14. Test Templates
@TestTemplate
メソッドは通常のテストケースではなく、テストケース用のテンプレートです。
そのため、登録されたプロバイダが返す呼び出しコンテキストの数に応じて、複数回呼び出されるように設計されています。
したがって、@TestTemplate
はTestTemplateInvocationContextProviderを実装したExtensionと併せて使用する必要があります。
テストテンプレートメソッドの起動は、通常の@Test
メソッドの実行と同じように動作し、同じライフサイクルコールバックと拡張機能を完全にサポートします。
使用例については、「Providing Invocation Contexts for Test Templates」を参照してください。
5.8. Providing Invocation Contexts for Test Templates
@TestTemplate
メソッドは、少なくとも1つのTestTemplateInvocationContextProviderが登録されている場合にのみ実行できます。
このようなプロバイダはそれぞれ、TestTemplateInvocationContextインスタンスのストリームを提供します。
各コンテキストは、カスタム表示名と、@TestTemplate
メソッドの次の呼び出しにのみ使用する追加拡張のリストを指定できます。
次の例では、テスト・テンプレートを記述し、TestTemplateInvocationContextProviderの登録方法と実装方法を示します。
@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String parameter) {
assertEquals(3, parameter.length());
}
static class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider {
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
return Stream.of(invocationContext("foo"), invocationContext("bar"));
}
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回呼び出されます。
呼び出しの表示名は、呼び出しコンテキストで指定された「foo」と「bar」になります。
各呼び出しは、メソッドパラメータを解決するために使用されるカスタムParameterResolverを登録します。
ConsoleLauncherを使用する場合の出力は次のとおりです。
└─ testTemplate(String) ✔
├─ foo ✔
└─ bar ✔
TestTemplateInvocationContextProvider拡張APIは主に、異なるコンテキスト—たとえば、異なるパラメータで、テストクラスインスタンスを別々に準備したり、コンテキストを変更せずに複数回作成したりすることができます。—でのテスト類似メソッドの繰り返し呼び出しに依存するさまざまなテストを実装するために主に使用されます。
@ParameterizedTest
が配列を引数として受け入れる場合の表示名を人間が読めるようにしました
多分このIssueの対応で、Javaで配列オブジェクトを単純に出力しようとすると[Ljava.lang.String;@7c29daf3
みたいな感じで中身見えないのを修正したのだと思います。
が、M4とM5で比較できるようなコードがちょっと書けなかったので検証は出来てません。すいません。
@EnumSource
の仕様が変更されました
M4では、@EnumSource
のnamesは取得対象のEnum名を指定していましたが、modeと言うプロパティがM5で追加されたことでnamesに渡された情報の意味を変更出来るようになりました。
Mode | namesの意味 |
---|---|
INCLUDE | デフォルト値。namesに指定したEnum名のみ適用 |
EXCLUDE | namesに指定していないEnum名のみ適用 |
MATCH_ALL | namesに指定した全ての正規表現に合致するEnum名のみ適用 |
MATCH_ANY | namesに指定した何れかの正規表現に合致するEnum名のみ適用 |
(独り言)そこまでして使いたいかねぇw
@MethodSource
の仕様が変更されました
@MethodSource
で指定したメソッドの戻り値にDoubleStream、IntStream、LongStreamを指定してもエラーにならないようになりました。
@TestFactory
の仕様が変更されました
@TestFactory
は、任意のネストされた動的コンテナをサポートするようになりました。詳細はDynamicContainerと抽象ベースのDynamicNodeを参照してください。
との事です。User Guideにサンプルも付いてました。
@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()))
))
)));
}
これをみて、DynamicTestをどう活かすかが何となく見えてきました。
DynamicTestは、外部DSLによって書かれたテストを構造化した一連のテストコードに変換し、適切にレポーティングできるようにするのを容易にする仕組みなんじゃなんですかね?
@BeforeAll
で発生した例外をAfterAllCallback実装インタフェースで捕捉できるようになりました
厳密に言うと、@BeforeAll
をアノテートしたメソッドもしくは、BeforeAllCallbackインタフェース実装メソッドで発生した例外という事らしいです。
意識してませんでしたが、M4では出来なかったと言うことですね。
調べて見ましたが、M4ではAfterAllCallback#afterAllの引数はContainerExtensionContextであり、発生した例外を取得するAPIは存在していませんでした。
M5からContainerExtensionContextはExtensionContedxtに統合されたため捕捉出来るようになったと言うことでしょう。
同じ理屈で言うと、@BeforeEach
で送出された例外はAfterEachCallback実装インタフェースで捕捉可能です。(M4時点で可能)
テストクラス間でStoreを経由した情報共有ができるようになりました
うん、良く分からん。
Extensions may now share state across top-level test classes by using the Store of the newly introduced engine-level ExtensionContext.
とのことですが、Storeの説明が少なすぎて...
API成熟度
M4の資料時点と変わった物/新規の物のみですが、以下の様になっております。
旧成熟度 | 新成熟度 | |
---|---|---|
@TestInstance |
- | 未記載 |
@TestTemplate |
- | Experimental |
Assertions#assertAll | Maintained | Experimental |
Assumptions#assumingThat | Maintained | Experimental |
DynamicContainer | - | Experimental |
DynamicNode | - | Experimental |
ExecutionCondition | - | Experimental |
TestTemplateInvocationContextProvider | - | Experimental |
TestTemplateInvocationContext | - | Experimental |
- assertAllについては、資料的にMaintainedのように記載していたのでここに記載していますが、M4の時点からExperimentalでした。assumingThatも同様の理由で記載しています。
基本的に表記漏れor追記ばかりで、マイルストーン毎には変化しないのかも知れませんね。
こんなもんかな?
User GuideのリリースノートとにらめっこしてChapter 7以外のところでの主な変更点はこんな感じになります。
最後に
私、最近発表する機会も多くないのでJUnit5頑張ります。少なくとも正式リリースまでは変更点を適宜お届けできればと思います。
次のマイルストーンであるM6ですが、2017/7/14日現在2017/7/16のリリース予定で進捗が42%らしいので多分遅れるでしょうw