読者です 読者をやめる 読者になる 読者になる

JUnit 5のUser Guideを写経してみた その1

ユニットテスト

JUnit 5のUser Guideをひととおり読んで写経してみたので、何回かに分けてまとめてみます。

リポジトリこちらです。

JUnit 5の特徴

簡単にまとめると(まとまってない)、こんなカンジです。

  • ラムダ式を活用しているJava 8前提)
  • hamcrestは積んでいないアサーションAPIはお好きに)
  • アノテーションが全部変わった (!?)
  • 任意のアクセス修飾子が使える(privateは無理っす)
  • 拡張を容易にする仕組みが設けられた(RuleとかTestRunnerがお役御免に。メソッドインジェクションできるようになった)
  • etc

Travis CI + Gradle + JUnit 5でテストが実行されない(UP-TO-DATE判定される)など、気になる点はケッコーあるものの、現時点でもそれなりに使えるな、という印象を受けました。

ちょっとした工夫は必要ですがIDEからの実行もできますし、ユニットテストの概念そのものに大きな変更はないので、正式版で互換性がなくなるかもしれないリスクを許容できるなら、という条件付きですが、すぐに乗り換えても大きな問題はないかも?

テストのライフサイクル

class StandardTest {

    @BeforeAll
    static void initAll() {
        // JUnit 4の@BeforeClassに相当。
        // staticにするのを忘れると、このクラスに書かれたテストが1つも実行されないので注意
    }

    @BeforeEach
    void init() {
        // JUnit 4の@Beforeに相当。
    }

    @Test
    void succeedingTest() {
        // JUnit 4の@Testに相当。
        // パッと見JUnit 4と同じアノテーションのように見えますが、FQCNは違うので要注意。
    }

    @AfterEach
    void tearDown() {
        // JUnit 4の@Afterに相当。
    }

    @AfterAll
    static void tearDownAll() {
        // JUnit 4の@AfterClassに相当。
        // こちらもstaticにするのを忘れると(ry
    }
}

要点をまとめると、以下のような感じです。

  • privateを除く任意のアクセス修飾子を付けられる
  • ライフサイクルの考え方はJUnit 4と変わらない
  • アノテーションが一新された

@BeforeClass@BeforeAllに、
@Before@BeforeEach に、
@After@AfterEach に、
@AfterClass@AfterAll に、
それぞれ変更されています。

パッと見 @Test は変わっていないように見えますが、FQCNが違います。
org.junit.Test ではなく org.junit.gen5.api.Test です。

junit5リポジトリのprototype-1タグを覗いてみると、@TestInstanceというライフサイクルに影響しそうなアノテーションもありますが、ALPHA版には搭載されていません(個人的にはいらないかな)。

大きな注意点としては、@BeforeAll、@AfterAllにstaticを付け忘れると、そのクラスに書かれたテストが1つも実行されないこと。
失敗もスキップも中断もされず、完全にガン無視されます。

staticを付けた場合の実行結果

Test run finished after 203 ms
[        45 tests found     ]
[         3 tests skipped   ]
[        42 tests started   ]
[         2 tests aborted   ]
[        40 tests successful]
[         0 tests failed    ]

staticを付けなかった場合の実行結果

Test run finished after 203 ms
[        45 tests found     ]
[         2 tests skipped   ]
[        40 tests started   ]
[         2 tests aborted   ]
[        38 tests successful]
[         0 tests failed    ]

tests foundの数はどちらも同じ45ですが、test startedとtest successful(ついでにtest skipped)の数が足りないです。 tests failedの数だけ気にしていると、staticを付け忘れたことによってガン無視されたテストがあることに気付かないので要注意です。
ちなみにJUnit 4では@BeforeClass@AfterClassにstaticを付け忘れた場合、initializationErrorになるので何かおかしいことに気付くことができます。 (次バージョンで修正されてるといいな)

テストケースの名前

@DisplayName("(^o^)ノ ")
class SampleTest {

    @Test
    @DisplayName("文字列なので、識別子として使用できない文字も使用できます!!!")
    void test1() {
    }
    
    @Test
    @DisplayName("EclipseのJUnitビューやテストレポートにはこの名前で出力されます。")
    void test2() {
    }
}

ご覧のとおりです。

注意点としては、EclipseのQuickJUnitプラグインを使用している場合、@DisplayNameを付けているとinitializationErrorとなって実行できません。 [Run As] や [Debug As]、[F11]キーなどで、QuickJUnitを介さなければ実行できます。ここはQuickJUnitの改善に期待ですね。

アサーション

class SampleTest {

    @Test
    void 通常のアサーション() {
        assertEquals("expected", "actual");
        assertNotNull(new Object());
        assertTrue(true);
    }
    
    @Test
    void 例外のアサーション() {
      IOException thrown = expectThrows(IOException.class, () -> {
          throw new IOException("メッセージ");
      });
      
      assertEquals("メッセージ", thrown.getMessage());
    }
    
    @Test
    void グループアサーション() {
        assertAll(
            () -> assertNotNull(null),
            () -> assertNull(""),
            () -> assertEquals("a", "b"),
            () -> assertNotEquals("a", "a"),
            () -> assertTrue(false),
            () -> assertFalse(true)
        );
    }
}
通常のアサーション

JUnit 4.4から標準搭載されているhamcrestライブラリは、JUnit 5には同梱されません。
そのため往年の assertEquals()assertNotNull()assertTrue()などを使ってアサーションします。
お望みなら別途hamcrestを導入することで、hamcrestマッチャーを使い続けることもできます。

例外のアサーション

例外のアサーションとしては、expectThrows()を使います。
第一引数に期待する例外の型を、第二引数に例外を発生させる処理(4フェーズテストのExerciseフェーズ)をラムダ式で渡すことで、例外の有無とその型をアサートすることができます。
また戻り値として発生した例外オブジェクトが返されるので、続けてgetMessage()getCause()などをアサートすることが可能です。

JUnit 4のExpectedExceptionでは、例外を発生させる処理(4フェーズテストのExerciseフェーズ)よりも前に、期待結果(Verifyフェーズ)を書かなくてはいけなかったため、すごくモヤモヤしながらテストを書いていましたが、そんなモヤモヤとお別れできて私としては超嬉しいです!

グループアサーション

グループアサーションは、1つのテストケースメソッドの中に複数アサーションを書く場合に使用します。こいつを使うことで、いずれかのアサーションが失敗した場合でも、後続のアサーションが中断されず、すべて実行されるようになります。

上記のテストケースを実行すると、失敗したテスト結果が以下のようにまとめて表示されます。

expected: not <null>
expected: <null> but was: <>
expected: <a> but was: <b>
expected: not equal but was: <a>
<no message> in org.opentest4j.AssertionFailedError
<no message> in org.opentest4j.AssertionFailedError

スキップと中断

class SampleTest {

    @Disabled("スキップする理由")
    @Test
    void skippingTest() {
    }

    @Test
    void abortingTest1() {
        // その1
        assumingThat(
            System.getProperty("os.name").toLowerCase().contains("windows"),
            () -> {
                // ここにアサーション書く
            }
        );
    }
    
    @Test
    void abortingTest2() {
        assumeTrue(System.getProperty("os.name").toLowerCase().contains("windows"));
        // ここにアサーション書く
    }
}

@DisabledJUnit 4の@Ignoreに相当します。tests skippedにカウントされます。
assumingThat()assumeTrue()(ついでにassumeFalse())はJUnit 4のassumeThat()の代替です。第一引数に渡すboolean(またはBooleanSuplierの戻り値)が条件を満たす場合のみ、テストが実行されます。条件を満たさない場合はtests abortedにカウントされます。
@BeforeAllメソッドに記述することで、そのクラスに書かれたテストケースを丸ごと飛ばすこともできます。

構造化テスト

class OuterClass {
    @Nested
    static class Windowsの場合 {
    
        @BeforeAll
        static void beforeAll() {
            assumeTrue(System.getProperty("os.name").toLowerCase().contains("windows"));
        }
    
        @Test
        void test1() {
        }
    }
    
    @Nestted
    static class Windows以外の場合 {
    
        @BeforeAll
        static void beforeAll() {
            assumeFalse(System.getProperty("os.name").toLowerCase().contains("windows"));
        }
        
        @Test
        void test2() {
        }
    }
    
    @Test
    void 共通のテストケース() {
    }
}

テストの性質ごとにクラス分けしたい場合(初期化・終了処理を共通かしたい場合など)、構造化テストが使えます。

JUnit 4の@RunWith(Enclosed.class)に相当しますが、プラス、JUnit 5では以下の利点があります。

  • 外側のクラス(上記のOuterClass)にもテストケースを書けるようになった。
  • 内部クラスをstaticにする必要がなくなった。

注意が必要なのは、Javaの言語的制限で、非staticなインナークラスにはstaticなメンバーを持つことができないので、 インナークラスを非staticにする場合は@BeforeAll@AfterAllのライフサイクルメソッドは定義できませんコンパイルエラーになります)。

次回予告

とりあえず今日はこんなところで。
次回はメソッドインジェクションと、defaultメソッドの活用を書く予定です。