Java
JUnit
junit5

JUnit5M4 -> M5の変更点

More than 1 year has passed since last update.

初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つのテストクラスで混在したときに変な問題が起こらなくするための修正となります。


ParameterizedNGTest.java

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は、テストメソッド実行毎にテストクラスを実体化していました。


TestInstancePerMethodTest.java

@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を指定)すると、テストクラスの実体化は一度しか行われなくなり結果が異なります。


TestInstancePerClassTest.java

@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の実行結果を以下に示します。


AssertAllInExceptionTest.java

@RunWith(JUnitPlatform.class)

public class AssertAllInExceptionTest {
@Test
void thrownNullPointerException() {
assertAll(
() -> {throw new NullPointerException();},
() -> assertEquals(2, 3)
);
}
}

M4の実行結果
M5の実行結果

スクリーンショット 2017-07-13 13.09.51.png
スクリーンショット 2017-07-13 13.14.23.png

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