JUnit 5 でどこが変わったのか

JUnit5 でどこが変わったのか、今いるチームの開発メンバや、JUnit5ざっくり知りたい人向けに書いておきます。 この記事では以下の内容について書いていきます。

  • なぜ移行するか
  • どこが変わったか
  • まとめ

なぜ移行するのか

なぜ移行するかを簡単に説明しておきます。 以下のような内容になってくるかなと思います。

  • やっぱり新しいライブラリ・ツールを使いたい
  • 新しい @ParameterizedTest を使いたい
  • Java 11のサポート

僕としては、これから何年かはJUnit5で書いていくことになると思うので、ツールを早く導入していきたいという気持ちがあり 「やっぱり新しいライブラリ・ツールを使いたい」という理由を挙げています。 また、junit-jupiter-params@ParameterizedTest を使いたい気持ちがあります。まだAPIとしては experimental ですが、 個人的には、以前のものと比べると、使いやすいなぁと思っています。 また、JUnit5はモジュールの構成が拡張のしやすさもあり、今後使っていきたいところです。

JUnit5はJava11のサポートがありますが、JUnit4では動く保証がありません。 そのため、Java11へ移行するのであれば、JUnit5へ移行していく必要があります。 とはいいつつ、JUnit4は今の所、新しいJavaのバージョンになっても、手元のアプリケーションでは、なんとなく動いています。 Javaのアップデートのために急ぐ必要はないと思っています。 また、JUnit5にはJUnit4で書かれたテストを動かす機能もあるので、テストコードの修正なしに Gradleなどのビルドツールの設定と依存関係を追加することによって Java11への移行がすんなり行えるかもしれません。

そんなこんなで、今すぐ変えよう!という感じではないのですが、やっぱり新しいものは使っていきたいね、というところで 「どこが変わったのか」について書いていきたいと思います。 「どうやって移行するのか」については、この記事を読んだあとに以下の記事を読んでいただければな、と思います。

どこが変わったのか

まずはざっくり、マイグレーションガイドを見つつ、自分が知っている変わった点の一覧を書いてみます。

  • アノテーションのパッケージ名とアノテーション自体の名前の変更
  • @Test にあった expectedtimeout の属性の削除
  • アサーションのパッケージ名変更とインターフェースの変更
  • 新しい拡張モデルとして、Extensionの追加
  • Extensionの追加に伴う、Rule, ClassRuleの廃止
  • テストコードに public な修飾子をつけなくてもよくなった
  • 4 --> 5 のマイグレーション用のモジュールの追加
  • JUnit 5向けのテストをJUnit4で動かすためのモジュール
  • EnclosedParameterizedTest が モダンな感じになった
  • JUnit5を動かすために、Gradleへの設定が必要
  • Java9 以降のJDKのバージョンへのサポート

色々ありますが、ドキュメントにも書かれていますが、 日本語の情報だと、irofさんという方の「どうしよう JUnit5」というスライドに大体書かれています。

この記事では、JUnit5と書いていますが、JUnit5ってなんなのかを表す言葉をユーザガイドから引用すると

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

になります。

JUnit Platformというテストを動かすためのAPIや実装で JUnit Jupiterは JUnitの5として新しいプログラミングモデルと拡張モデルのAPIとPlatformで動かすためのEngineの実装が含まれています。 JUnit Vintageは JUnit Platform上で JUnit4のプログラミングモデルをPlatformで動かすためのEngineの実装です。 また、JUnit PlatformにはIDEやビルドツールへの対応も含まれており、開発者は、自分達でEngineを書いて 好きな環境で動かすことも可能になっています。僕の知っているEngineの実装としては jmockitというライブラリがあるのですが、その中にEngineの実装があります。 余談ですが、元々jmockitoはJUnit4のクラスにモンキーパッチを当てていましたが 一段抽象化が入ることにより、ダーティなハックをすることなく、強力なモックを使うことが出来るようになっていそうです。 実際使ってことないのでわかりませんがw

とまぁ、本題からずれてしまったのですが、より1段階抽象化されており JUnit4と5が共存出来る形になっています。

アノテーションのパッケージ名とアノテーション自体の名前の変更

アノテーション周りが org.junit から org.junit.jupiter.api に変更されています。 また、@Ignore@Disable になっていたりと 細かい変更はありますが、基本的にはパッケージの移動が行われています。

また、@Test にあった expectedtimeout の属性の削除なども行われており こちらは、アサーションによって、テストするように変更されています。

どう書き換えていくかというと、以下のようなイメージです。

- @Test(expected = HogeException.class)
+ @Test
public void testThrows() {
-    foo()
+    Exception e = assertThrows(HogeException.class, () -> foo());
+    assertEquals("Hoge", e.getMessage());
}

timeoutについては省略しますが、assertTimeoutというアサーションjunit-jupiter に追加されています。 そちらを使って書き換えることになるでしょう。

アサーションのパッケージ名変更とインターフェースの変更

org.junit.jupiter.api.Assertionsアサーションが移動しています。 また、assertThat の廃止がされています。 加えて、

新しい拡張モデルとして、Extensionの追加

Extensionという新しいインターフェースが追加されています。 JUnit4でいうSpringRunnerだと、SpringのチームからSpringExtensionが追加されていたり MockitoのチームからはMockitoExtensionが提供されています。 順次サポートが広がってきており、エコシステムも充実して使える状態になってきています。

加えて、Extensionの追加に伴う、Rule, ClassRuleが廃止されています。 この辺は、自分で書いている場合は自分で移行したり、ライブラリの開発チームから提供されるのを待つ必要があります。 ただ、Extensionになって、簡単に書けるようになっているので 自分で実装を追加して、ライブラリにコントリビュートしても良いかもしれないですね

Extensionについては以下のUserGuideを見てほしいのですが、RunWithと違う点を一つ書いておきます。

JUnit4では、RunWithでは一つしか指定できなかった拡張モデルを、Extensionでは複数指定できるようになっています。

@ExtendWith(DatabaseExtension.class)
@ExtendWith(WebServerExtension.class)
class MySecondTests {
    // ...
}

また、JUnit5からは全体的に合成アノテーションのサポートがされており 以下のような @LargeTest のようにアノテーションをまとめる事ができます。 この辺はSpringをがっつり使っている方には馴染み深いものかと思います。 ドキュメント的にはComposed Annotationとして紹介されています。

ドキュメントには以下のリンク先に書かれています。

@ExtendWith(DatabaseExtension.class)
@ExtendWith(WebServerExtension.class)
// その他必要なアノテーションを省略
@interface LargeTest {} // DatebaseExtensionとWebServerExtensionをまとめるアノテーション

@LargeTest
class MySecondTests {
    // ...
}

このExtensionのモデルは非常に拡張しやすい形になっているのですが Rule, ClassRuleで出来ていたテストコード側で状態を持つような拡張がしづらくなったかなぁと思っています。

追記: 2019/07/03 この点は RegisterExtension (JUnit 5.1.1 API) を使えば良さそう

どんなコードかというと、WireMockのRuleがそれに当たります。

@Rule
WireMockRule wireMock = new WireMockRule(WireMockConfiguration.wireMockConfig().dynamicPort());

これを移行する方法を考えた時にどうするか、というと Extensionの定義に加えて、Extensionに設定を伝えるアノテーションやルールが必要になってきます。 wiremock本体ではないですが、コミュニティから出ている、wiremock-extensionであれば、以下のような形に書き換えることになります。

@Managed
WireMockServer wireMock = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());

という風に、いくつか不便になりましたが、 Extension自体が簡単に書きやすくなった、というのもあるので トレードオフという感じですね。

これからJUnit5向けにExtensionが出てくると思うので 移行期間はJUnit4で動かすのか、JUnit5で頑張って動かすのか考えつつ 移行していきたいですね。

テストコードに public な修飾子をつけなくてもよくなった

地味な変更です。

テストメソッドやテストクラスに public をつける必要がありましたが、必要ありません。 テストコードのノイズが減って良いですね。

- public class FooServiceTest {
+ class FooServiceTest {

    @Test
-    public void test() {
+    void test() {
        // ...
    }

}

変えなくても動くので、古いコードについては別にそのままで良いかなぁと思います。 新しいテストについては消していきたい気がしますが、好みのレベルと思います。

4 --> 5 のMigration用のモジュールの追加

いくつか、マイグレーション用のモジュールがあります。

  • JUnit5でJUnit4のテストを動かすための JUnit Vintage
  • JUnit4でJUnit5のテストを動かすための @RunWith(JUnitPlatform.class)
  • JUnit5でJUnit4の機能を一部再現するための junit-jupiter-migrationsupport

といったように、マイグレーションをサポートするモジュールや段階的に切り替えていくためのモジュールがたくさん用意されています。 規模が大きいのであれば、これらを駆使して移行していきたいですね。

EnclosedParameterizedTest が モダンな感じになった

Enclosed という 実はExperimentalだったアノテーションがあるのですが このアノテーションを使ってテストを構造化すると、以下のように少し複雑な形になります。 publicstatic が必要なのもあってかちょっと冗長に見えます。

@RunWith(Enclosed.class)
public class OuterTest {

    public static class InnerTest {
    }
    
}

Nested という追加されたアノテーションを使って構造化すると以下のようになります。

class OuterTest {

    @Nested
    class InnerTest {
    }
}

修飾子が要らなくなったのも合わせて対応してみると、非常に簡潔に構造を示すことが出来ます。 ただ、Enclosed と比べて注意する点が一つあります。 それは InnerClassstatic をつけていると動きません。 移行時に注意しておきたいですね。

@ParameterizedTest について書こうと思ったんですが ドキュメント見ろ、で終わる話なので詳しく書きませんが 非常に便利になったと思います。 以前書こうと思ったら、残念すぎて心が折れた記憶があります。

JUnit4でも、サードパーティのモジュールに JUnitParams というのがありますが そっちを使えば、良い感じに書けるそうですが やっぱり、公式でモダンな形でサポートされているのは強いですね。

詳しくは下のリンクを見て下さい

JUnit5を動かすために、Gradleへの設定が必要

Gradleのデフォルトのテストでは、JUnit4で動くようになっており GradleでJUnit5を動かすには設定の追加が必要です。 ただ、新しいプロジェクトでは以下のように、簡単に設定が出来ます。

test {
    useJUnitPlatform()
}

もちろん、Jacocoの対応もばっちりです。 Gradle 4.6から使えるようになっているので、Gradleのアップデートがまだな方はアップデートしていきましょう。

移行期間に伴い、JUnit4で動かしたい場合は、JUnit Vintageを使う必要があります。 以下にような形で設定を追加すれば良いでしょう。

test {
    useJUnitPlatform {
        includeEngines 'junit-jupiter', 'junit-vintage'
    }
}

MavenMavenで設定が必要になるとは思います。

詳しくは以下のリンク先を見てください

まとめ

今回の記事では、ざっくり知っている範囲について、メモとして書きました。 JUnit5のプログラミングモデル どうやって移行したか、については以前書いた記事をご覧ください。

以前書いた記事では、全部書き換える手法を取りましたが、成長したプロダクトでは 一気に書き換えるとPRのサイズがめちゃくちゃでかくなります。 上の記事で書いた、移行したプロジェクトのサイズは、そこそこ小さかったはずですが、+-共に2000行程度になり、4000行程度の差分が生まれました。 (僕は、同僚氏にでかいPRを投げつけました、の札。(画像略)) そのため、本来はゆっくり移行すべきかと思います。

また、前回の記事から、もう一つSpring Bootで書かれたサーバのプロジェクトを移行しましたが 大体似たような移行方法になったので、記事にはしないことにしました。

今度はJUnit Vintageを使って段階的に移行する方法を取ってみて、記事を書いてみようかなと思います。