LoginSignup
6
5

More than 5 years have passed since last update.

JUnit5M4 -> M5の変更点

Posted at

初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

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5