ちょっと話題の記事

JUnit5はどこに向かうのか?

2015.11.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

渡辺です。

【速報】JUnit5 はこうなる!?【プロトタイプ】」ではJUnit5 (JUnit Lambda)は、JUnit4の設計を継承しつつ、全く別のフレームワークとしてデザインされていることを紹介しました(現在はプロトタイプであり、今後の変更の可能性は十分にありえます)。 Java界隈から離れている人も多い中、多くの反響があるあたり、腐ってもJavaといったところでしょうか。

Twitterや、はてぶコメントを見る限り、「ドラスティックな変更方針にはポジティブな反応が多めだが、移行や普及の部分に関してはネガティブな意見もある」といった雰囲気です。 ネガティブ意見の方が声が大きいことを考慮すると、概ね8割程度がポジティブ、2割程度ネガティブといった印象を受けます。

さて、今回のエントリーでは、一開発者としての視点で、JUnit5に移行する場合の問題や課題について考えてみたいと思います。 重ねてお断りしますが、この内容は、現時点で公開されているプロトタイプの方針が概ね採用された場合の話です。 「いや、こういう方針がいいだろう」という意見は、フィードバックとしてJUnitチームに投げてください。

JUnit5はJava8以上を必要としている

JUnit5の展望を考える時に、最も重要な前提条件は、Javaのバージョンです。 JUnit5がLambda式対応である以上、Java8が必須となることは間違いありません。 2015年末の段階で、Java8のリリースから1年半以上が経過していますが、現場でJava8がどの程度まで普及しているのでしょうか?

また、いわゆる大規模SI案件の現場とそれ以外の案件の現場では、わける考える必要があります。 大規模SI案件では、最新バージョンの採用は遅いと言わざるを得ません。 近年ではAWSなどのクラウド環境を利用する場合も増えているため、ハードウェアやOS起因のJavaバージョン制約は減ってきたと思います。 とはいえ、これまでの歴史を考慮すると、Java8が大規模SI案件で一般的になるには5年以上はかかるのではないでしょうか? つまり、当面の間は大規模SI案件ではJava5(系)+JUnit4がスタンダードだと思います。

一方、SI案件でも小規模の新規案件やシガラミの少ない案件であったり、非SI案件ではJava8がスタンダードとなるのに多くの時間はかからないと思います。 自分の観測範囲では、あえてJava7を選択するケースはありません。 そもそもJavaを使うかどうかという話もあるにせよ、「Javaを使うならばJava8(以上)」という点に異論はないはずです。 なぜならば、Lambda式導入によって、やっとJavaでも手続き的なコードの呪縛から解き放たれたからです。 Javaは使うけどJava8にできないならばGroovy、Java8でいいならばJava8、自分ならばそんな感覚です。

というわけで、以下、大規模SI案件の話はしません。 これからJava8でJUnit5を導入できるチームにおける課題などを考えてみたいと思います。 結論から言えば、Java8をスタンダードとしている現場でJUnit5が利用されて普及していくならば、様々な懸念点は解決していくと思います。

JUnit4とJUnit5との比較

はじめに、JUnit4とJUnit5を単純に比較してみます。 JUnit4ユーザがJUnit5を導入するとした場合、どの程度の混乱が発生するかを考えてみましょう。 JUnit5(プロトタイプ)の概要については先日のエントリーで紹介したので、ザックリ解るように表にしてみました。

内容 JUnit4 JUnit5(Prototype)
テストクラスのアクセサビリティ public public制限無し
テストクラスのラフサイクル テスト毎に作成 テスト毎か再利用か選択可
テストメソッド @Test @Test
テスト名 TestName (Rule) @Name, @TestName
初期化処理 @BeforeClass, @Before @BeforeAll, @BeforeEach
終了処理 @AfterClass, @After @AfterAll, @AfterEach
構造化テスト Enclosed (TestRunner) @Nested
カテゴリ化テスト @Categories @Tag, @Tags
パラメータ化テスト @Theories, @Parameterized 提供されない予定
テストの無効化 @Ignore @Disabled
例外のテスト expected, Rule expectThrows
アサーション Hamcrest 基本アサーションのみ

この表から解るように、一部の機能を除けばJUnit4の機能は継承されています。 したがって、JUnit4を理解していれば継承された機能をJUnit5に移行することは難しくないでしょう。 最初は多少の混乱はあるかと思いますが、すぐに慣れるレベルかと思います。 逆に、新しくJUnit5からJavaのユニットテストに入るのであれば、JUnit4の制約がないことは良い材料です。

特に、構造化テスト(ネストクラス)の時、JUnit4ではネストクラスをstaticクラスにすることを強いられていました。 これは、テストクラスをテスト毎に作成するという制約があったためです。 この制約がある以上、テストクラスからアウタークラスのインスタンス変数にアクセスできませんでした。 ユニットテストではテスト毎にテストインスタンスを作成することが原則なので、この制約は仕方ないと考えても良いでしょう。 しかし、テストがネストクラスに構造化された場合は、アウタークラスのインスタンス変数がコンテキストに含まれる方が自然だったりします(実際に、アクセスしたいケースは多々あります)。 この問題はJUnit5では改善され、ネストクラスからのインスタンス変数へのアクセスが可能になる見込みです。

逆にパラメータ化テストやHamcrestのMatcherなどはJUnit5では提供されない見込みです。 これは、JUnit5の基本方針として、コア機能を最小限に絞り込み、拡張性を高くしてサードパーティ製ライブラリを使いやすく作りやすくする方針であることも起因しています。 パラメータの定義方法や使い方などは状況によって適する仕組みが異なることもありますし、アサーションなどは好みの問題となりがちです。 これらの部分は切り離して好きなライブラリを選択できるようにしています。

また、JUnit4ではJUnit3時代の設計をベースに、多様化するユニットテストの手法に対応するため、多くの魔改造が行われていた経緯があります。 そして、Javaは制約の厳しい言語であるため、魔改造を行っても制約がつきまといます。 JUnit4を捨てて被るデメリットよりも、JUnit4を捨てて得るメリットの方が多いのです。

Matcherの廃止がもたらすもの

テスティングフレームワークの評価基準で、アサーションの使いやすさは重要な部分です。 特に「期待された状態か判定する」Matcherは多種多様で、ぶっちゃけ好き嫌いの世界だったりします。 一方、ユニットテストの基本構成自体は、言語が同じであれば、各(ユニット)テスティングフレームワークで大きな違いになりません。

さて、JUnit4では、外部ライブラリであるHamcrestライブラリを導入するに至りました。 Hamcrestにより、新しいアサーションとなる「assertThat」と比較検証を行うMatcherをインターフェイスが導入されます。 Hamcrestのアサーションでは、「assertThat(acrual, is(excpected));」と自然言語風に読めることも大きな特徴です。 これは、それまでassertTrueやassertEqualsといった基本的なアサーションしか提供していなかったJUnitに大きな変化をもたらしました。 自書「JUnit実践入門」でも、HamcrestのアサーションをJUnit4のスタンダードとして紹介しています。 しかし、Hamcrestのこれらの仕組みは独特である故、好みが分かれる部分でした。 自分はHamcrest Loveですが、頑なに「嫌い」という意見も理解できます。

JUnit5では、基本的なアサーションは提供しますが、Hamcrestで提供されていたアサーションは切り離す方針です。 もちろん、この後のフィードバックにより「やっぱ、Hamcrestいれるわー」となるかもしれませんし、「junit5-hamcrest」のように拡張ライブラリとして提供される可能性もあります。 今の所、この方針については、ポジティブ意見が多いようです。 従来通りHamcrestを利用したければHamcrestを導入すれば制約無く利用することができます。 JUnit4からの移行コストを減らすのであれば、Hamcrest + assertThatをプロジェクトの方針とすれば、多くのエンジニアがスムーズに移行できるはずです。 プロジェクトの方針で、他のアサーション系ライブラリを利用すると決めることもできるでしょう。

この問題はJavaのユニットテストにおけるモックライブラリやパラメータ化テストに関しても同様です。 モックでは個人的にはMockitoが好きですが、他のモックライブラリが好きな人も多く居ます。 パラメータ化テストは@Paramterisedも、@Theoriesもイマイチ使いにくいと感じます。

JUnit5では、JUnit自体をユニットテストのコアフレームワークとして提供し、モック・アサーション・パラメータ化はプロジェクトで選択するという方針に賛同します。 また、それらを作りやすくなるように拡張ポイントが設計されているので安心してください。

JUnit5のテストを実行する

JUnit5のプロトタイプでは、JUnit5のテストをコマンドラインやMavenやGradleで実行することができます。 しかし、IDEA, NetBeans, EclipseなどのIDEで簡単に実行出来なければ、JUnit4からの移行したいと思わないでしょう。

自分は、IDEのサポートは時間の問題だと、楽観的に考えています。 JUnit5のβ版がリリースされる頃には、一部のIDEではプラグインが提供されるのではないでしょうか?

なお、JUnit5のテストコードは、JUnit5 TestRunnerを利用することでJUnit4から実行することも可能です。 どういうこと?wって思いますが、JUnit4のTestRunnerは、テストの実行を魔改造する仕組みです。 テスト実行部分はJUnit5を利用して、結果をJUnit4形式にフォーマットしているにすぎません。

@RunWith(JUnit5.class)
public class AJUnit5TestCaseRunWithJUnit4 {
    @org.junit.gen5.api.Test
    void aSucceedingTest() {
        /* no-op */
    }
}

とはいえ、JUnit4案件でこっそりJUnit5のコードを紛れ込ませることはゼッタイにやめましょうw

拡張ポイント

JUnit4では、RuleとTestRunnerという仕組みが提供され、テストの拡張を行うことができました。 どちらも、テストの実行前と実行後に共通の処理を埋め込むための仕組みですが、Ruleではテストクラスのインスタンス変数を、TestRunnerはテストの実行をカスタマイズします。 JUnit5では、RuleとTestRunnerは廃止され、まったく新しいExtentionという仕組みに置き換わります。

Ruleができた背景はテストメソッドの引数制約です。 JUnit4ではテストメソッドは引数を持たないpublicメソッドと決まっていたため、テストの実行前に処理を行い値を保持するにはインスタンス変数を利用するしかありませんでした。 本来はテストメソッドに渡したい引数であっても、Ruleとしてインスタンス変数に定義していたのはそのためです。

Ruleを利用しない場合、テストの実行そのものをオーバーライドする必要があります。 このため、Theoriesでパラメータ化テストを行う場合、@Testではなく、TestRunner自体を差し替えることになります。 TestRunnerではテストの根本的な振る舞いを変えることもできます。 しかし、複数のTestRunnerを定義できないという問題もありました。

JUnit5では、このような制約をリセットする方針です。 Extentionでは、テストの実行前と実行後の処理、そしてパラメータを拡張することができます。 当然、幾つでも拡張することができるようになります。 いびつな設計(仕方ありませんでした)は整理されたわけです。

なお、ネストクラスによるテストの構造化やテスト名に対するアクセスは、フレームワークレベルで対応されます。

JUnit4のエコシステムへの懸念

最後になりますが、JUnit4をとりまくエコシステムに対する懸念についてです。 自分も最初にJUnit5を見た時はエコシステムを破壊するのでは?と大きな懸念があったのですが、色々と考えてみると懸念なんてほとんどありませんでした。

アサーションやモックは元来依存性が低い

アサーションやモックなどについては、元々ユニットテストのフレームワークとは切り離して考える部分です。 JUnit4でHamcrestがスタンダードとなっただけであり、JUnit4で他のアサーションフレームワークを使うことも可能でした。 モックライブラリについても同様です。 より便利に使うためのJUnit5 Mockito Extentionのようなサポートはあればベターですが、簡単に作ることもできます。

CIでの実行はビルドツールで吸収

テストの実行については、IDEさえサポートするのであれば、MavenやGradleで実行するのが当たり前です。 JenkinsのようなCIで実行することに何の問題もないでしょう。 唯一問題となるのは、テストレポートになりますが、そもそもJUnit4自体がantで実行した結果のxmlをCIなどで読み込めるようにしたに過ぎません。 CIで他の言語のテストを行った場合でも、Javaのant実行JUnit結果xml形式に変換するくらいです。 JUnit5の実行結果も合わせればいいだけなので大きな問題にならないでしょう。

エコシステムが大きな打撃を受けるのは、そのシステムのコア機能に依存していた場合と言えます。 JUnit4のコア機能に依存していたシステムというのはほとんどないのです。 JUnit5に移行したところで、HamcrestやMockitoが動かなくなることはありませんし、JenkinsやMavenは設定を変えれば動くことでしょう。

まとめ

チームとしては変わらないけど監督とスタメンは総入れ替えといったJUnit5ですが、ドラスティックな方針転換のデメリットはほとんどないと思います。

新しいものをとりいることができるチームでは積極的にJUnit5対応を行ったとしても問題ないでしょう。 導入する時期については、やや保守的なチームであれば各種拡張やツール類の対応や情報が出そろってからでも良いと思います。 革新的なチームや個人であればアーリーアダプタとして開拓しても面白いのではないでしょうか? 一方、大規模SI案件などでは、当面の間、従来通りにJUnit4を利用すればいいと思います、利用しているならば。