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 } }
要点をまとめると、以下のような感じです。
@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")); // ここにアサーション書く } }
@Disabled
はJUnit 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
のライフサイクルメソッドは定義できません(コンパイルエラーになります)。