JUnit 5の利点を活用する

この記事の別のバージョンがOracle blogで先に公開されています。

JUnit 5がリリースされてからすでに数年が経ちます。まだ開発テストでJUnit 5を利用していないなら、利用するべきです。JUnit 5には、時間を節約し問題を減らすのに役立つさまざまな新機能や改善があります。JUnit 5の利用を開始し、最新のテクノロジーの恩恵を受ける方法を説明します。

JUnit 4からJUnit 5に移行するべき理由

すでにそれなりの間JUnit 4を使い続けているのであれば、テストの移行は困難なタスクに見えるかもしれません。よいニュースとしては、おそらく既存のテストを変換する必要はまったくないでしょう。JUnit 5はVintageライブラリを使用してJUnit 4のテストを実行できるため、新しいテストをJUnit 5で作成するだけでよいのです。

JUnit 5を使い始めるべき確固とした理由は4つあります。

  • JUnit 5はラムダ関数などのJava 8以降の機能を活用し、テストをより強力に、また保守を容易にします。
  • JUnit 5には、テストの説明、分類、実行に役立つ機能が追加されています。たとえば、わかりやすいテスト名を表示したり、テストを階層構造で分類することができます。
  • JUnit 5は複数のライブラリで構成されているため、必要な機能だけをプロジェクトにインポートできます。MavenGradleなどのビルドシステムを使用すると、適切なライブラリを簡単にインクルードできます。
  • JUnit 5は1度に複数の拡張を使用できますが、これはJUnit 4では不可能です(JUnit 4は1度に1つのランナーしか使用できません)。つまり、JUnit 5はSpring拡張とその他の拡張(ユーザー独自の拡張など)を組み合わせるのが容易です。

JUnit 5に移行する方法

既存のJUnit 4のテストがあっても、JUnit 4からJUnit 5に移行するのは非常に簡単です。新しい機能が必要でないかぎり、既存のJUnit テストを JUnit 5に変換する必要はないでしょう。

  1. ライブラリとビルドシステムをJUnit 4からJUnit 5にアップデートします。既存のテストを実行できるよう、必ずJUnit-vintage-engineをテストのランタイムパスに含めてください。
  2. JUnit 5の構成を使って新しいテストをビルドします。
  3. (任意)JUnit 3およびJUnit 4のテストをJUnit 5に変換します

JUnit 5とJUnit 4の重要な違い

一見すると、JUnit 5のテストはほとんどJUnit 4と同じですが、注意するべき違いがいくつかあります。

インポート

JUnit 5のアノテーションおよびクラスは新しいorg.JUnit.jupiterパッケージを使用します。たとえば、org.JUnit.Testはorg.JUnit.jupiter.api.Testになります。

アノテーション

@Testアノテーションのパラメーターはなくなりました。これらはそれぞれ関数に変更されました。たとえばJUnit 4では、例外をスローすることが期待されるテストは次のように記述されました。

@Test(expected = Exception.class)
public void testThrowsException() throws Exception {
    // ...
}

JUnit 5では次のように変更されました。

@Test
void testThrowsException() throws Exception {
    Assertions.assertThrows(Exception.class, () -> {
        // ...
    });
}

同様に、タイムアウトも変更されました。JUnit 4では、タイムアウトは次のように記述されました。

@Test(timeout = 10)
public void testFailWithTimeout() throws InterruptedException {
    Thread.sleep(100);
}

JUnit 5では、タイムアウトは次のように記述されます。

@Test
void testFailWithTimeout() throws InterruptedException {
    Assertions.assertTimeout(Duration.ofMillis(10), () -> Thread.sleep(100));
}

その他のアノテーションの変更は以下のとおりです。

  • @Beforeは@BeforeEachに変更されました。
  • @Afterは@AfterEachに変更されました。
  • @BeforeClassは@BeforeAllに変更されました。
  • @AfterClassは@AfterAllに変更されました。
  • @Ignoreは@Disabledに変更されました。
  • @Categoryは@Tagに変更されました。
  • @Ruleおよび@ClassRuleはなくなりました—代わりに@ExtendWith および@RegisterExtensionを使用します。

アサーション

JUnit 5のアサーションはorg.JUnit.jupiter.api.Assertionsに含まれています。assertEquals()やassertNotNull()などのよく利用されるアサーションの大半は以前と同じように見えますが、重要な違いがいくつかあります。

  • エラーメッセージは最後の引数になりました。例: assertEquals(“my message”, 1, 2) は assertEquals(1, 2, “my message”)になります。
  • 多くのアサーションは、エラーメッセージを組み立てるラムダを取るようになりました。これは、アサーションが失敗したときにだけ呼び出されます。
  • @Timeoutアノテーションの代わりにassertTimeout()およびassertTimeoutPreemptively()を使用します(@TimeoutアノテーションはJUnit 5にも存在しますが、動作はJUnit 4のものと異なります)。
  • 後で説明するように、いくつかのアサーションが追加されています。

JUnit 4のアサーションをJUnit 5のテストで使うこともできることに注意してください。

前提条件

前提条件はorg.JUnit.jupiter.api.Assumptionsに移されました。

JUnit 4の前提条件はそのまま存在しますが、条件の比較にHamcrestだけでなく BooleanSupplierもサポートするようになりました。条件を満たす場合に実行するコードとして(Executable型の)ラムダを使用できます。

次は JUnit 4の例です。

@Test
public void testNothingInParticular() throws Exception {
    Assume.assumeThat("foo", is("bar"));
    assertEquals(...);
}

JUnit 5では次のようになります。

@Test
void testNothingInParticular() throws Exception {
    Assumptions.assumingThat("foo".equals("bar"), () -> {
        assertEquals(...);
    });
}

JUnitの拡張

JUnit 4では、フレームワークをカスタマイズする場合、一般的には@RunWithアノテーションを使用してカスタムランナーを指定していました。複数のランナーを利用すると問題が多く、通常はチェーンまたは@Ruleを使う必要がありました。JUnit 5 では、拡張を利用することでフレームワークのカスタマイズが容易になりました。

たとえば、JUnit 4でSpringフレームワークを使用したテストをビルドする場合、次のようになります。

@RunWith(SpringJUnit4ClassRunner.class)
public class MyControllerTest {
    // ...
}

JUnit 5では、Spring Extensionをインクルードします。

@ExtendWith(SpringExtension.class)
class MyControllerTest {
    // ....
}

@ExtendWithアノテーションは繰り返すことができるので、簡単に複数の拡張を組み合わせることができます。
また、1つまたはそれ以上のorg.JUnit.jupiter.api.extensionのインターフェイスを実装するクラスを作成し、@ExtendWithを使用してテストに追加することで、独自のカスタム拡張を簡単に定義できます。

テストをJUnit 5に変換する

たいていのJUnit 3またはJUnit 4の既存のテストは、次の手順でJUnit 5に変換できます。

  1. インポートからJUnit 4を削除し、JUnit 5を追加します。たとえば、@Testアノテーションのパッケージ名を更新し、アサーションのパッケージおよびクラス名を変更します(Asserts から Assertionsに)。コンパイルエラーが出ても心配はいりません。次の手順を行えば、エラーは解消されるはずです。
  2. 古いアノテーションおよびクラス名を使用している箇所をすべて新しい名前に変更します。たとえば、すべての@Beforeを@BeforeEachに変更し、すべてのAssertsをAssertionsに変更します。
  3. アサーションを変更します。メッセージを出力するアサーションは、メッセージの引数を最後に移動する必要があります。3つの引数がすべて文字列の場合に特に注意してください。また、タイムアウトおよび期待される例外を変更します(上の例を参照してください)。
  4. 前提条件を使用している場合は変更します。
  5. @RunWith、@Ruleまたは @ClassRuleを適切な@ExtendWithアノテーションに置き換えます。使用している拡張などの更新されたドキュメントをオンラインで探す必要があるかもしれません。

パラメータライズされたテストを移行する場合、特にJUnit 4のParameterized を使用している場合は、さらに追加で多少のリファクタリングが必要になります(JUnit 5のパラメータライズドテストのフォーマットはJUnitParamsに近いものです)。

新機能

ここまでは、既存の機能がどのように変更されたかだけを説明してきました。しかし、JUnit 5にはテストの記述性と保守性を高める新機能が多数あります。

表示名

JUnit 5では、クラスおよびメソッドに@DisplayNameアノテーションを追加できます。この名前はレポートを生成するときに使われ、テストの目的を説明したり、欠陥を追跡するのに役立ちます。例:

@displayName("Test MyClass")
class MyClassTest {
    @Test
    @DisplayName("Verify MyClass.myMethod returns true")
    void testMyMethod() throws Exception {
        // ...
    }
}

表示名ジェネレーターを使用してテストクラスやメソッドを処理し、指定したフォーマットでテスト名を生成することもできます。指定方法およびサンプルについてはJUnit ドキュメントを参照してください

アサーション

JUnit 5では、以下のような新規アサーションが導入されました。

  • assertIterableEquals()は、equals()を使用して2つのIterableに対して深い検証を実行します。
  • assertLinesMatch()は、2つの文字列のリストが一致しているかを検証します。expected引数には正規表現を指定できます。
  • assertAll()は、複数のアサーションをグループ化します。このアサーションの利点は、個々のアサーションが失敗した場合でもすべてのアサーションを実行できることです。
  • @Testアノテーションのexpectedプロパティの代替としてassertThrows()およびassertDoesNotThrow()が追加されました。

ネストされたテスト

JUnit 4のテスト スイートも便利ですが、JUnit 5のネストされたテストはもっとセットアップと保守が簡単で、テストグループ間の関係をよりわかりやすく記述できます。例:

@DiplayName("Verify MyClass")
class MyClassTest {
    MyClass underTest;
    
    @Test
    @DisplayName("can be instantiated")
    public void testConstructor() throws Exception {
        new MyClass();
    }
    @Nested
    @DisplayName("with initialization")
    class WithInitialization {
        @BeforeEach
        void setup(){
            underTest = new MyClass();
            underTest.init("foo");
        }
        
        @Test
        @DisplayName("myMethod returns true")
        void testMyMethod() {
            assertTrue(underTest.myMethod());
        }
    }
}

上の例では、MyClassに関するすべてのテストが1つのクラスにまとめられているのがわかります。外側のテストクラスでクラスがインスタンス化可能であることを検証し、ネストされた内側のクラスには、MyClassがインスタンス化され、初期化された状態で実行するすべてのテストがあります。@BeforeEachメソッドは、ネストされたクラスのテストにだけ適用されます。

テストおよびクラスの@DisplayNamesアノテーションは、テストの目的と構成を示します。こうすると、テストが実行された条件(初期化された状態でのVerify MyClass)およびテストで検証されたこと(myMethodがtrueを返している)がわかるため、テストレポートの理解が容易になります。これはJUnit 5の良いテスト設計パターンです。

パラメータライズドテスト

JUnit 4でも、JUnit4Parameterizedのようなビルトインライブラリや、JUnitParamsのようなサードパーティ製ライブラリによるパラメータライズ機能は存在していました。JUnit 5では、パラメータライズドテストが完全にフレームワークに組み込まれ、JUnit4ParameterizedやJUnitParamsの優れた機能も採用されました。例:

@ParameterizedTest
@ValueSource(strings = {"foo", "bar"})
@NullAndEmptySource
void myParameterizedTest(String arg) {
    underTest.performAction(arg);
}

書式はJUnitParamsに似ており、テストメソッドに直接パラメーターが渡されます。テストで使用する値をいくつかのソースから取得できることに注意してください。ここでは、パラメーターが1つしかないので、@ValueSourceを使用すると簡単です。@EmptySourceと@NullSourceは、それぞれテストを実行する値のリストに空文字列とnullを追加することを示しています(上の例のように両方を使い、2つを組み合わせることもできます)。ほかにも、@EnumSourceや@ArgumentsSource(カスタム値プロバイダー)などの値ソースがあります。複数のパラメーターが必要な場合、@MethodSourceまたは@CsvSourceも使用できます。詳細およびサンプルについてはJUnit 5のドキュメントを参照してください

JUnit 5で追加されたもう1つのテストタイプ@RepeatedTestでは、1つのテストを指定の回数繰り返して実行できます。

条件付きテスト実行

JUnit 5では、テストまたはコンテナー(テストクラス)を条件に従って有効化または無効化するExecutionCondition拡張を利用できます。これは、テストに対して@Disabledを付けるのに似ていますが、カスタム条件を指定できます。以下のようなビルトイン条件があります。

  • @EnabledOnOsおよび@DisabledOnOs: 指定されたOSでだけテストを有効化します。
  • @EnabledOnJreおよび@DisabledOnJre: Javaの特定のバージョンでテストを有効化または無効化します。
  • @EnabledIfSystemProperty: JVMシステムプロパティの値に基づいてテストを有効化します。
  • @EnabledIf: スクリプト化されたロジックを使用し、スクリプトで指定された条件を満たす場合にテストを有効化します。

テストテンプレート

テストテンプレートは通常のテストではありません。テストテンプレートは実行するべきステップのセットを定義します。他の場所で特定の呼び出しコンテキストを使用してテンプレートを実行します。つまり、テストテンプレートを1度定義したら、実行時に呼び出しコンテキストのリストを構築してテストを実行できます。詳細およびサンプルについてはJUnit 5のドキュメントを参照してください

動的テスト

動的テストはテストテンプレートと同様に、実行するテストが動的に生成されます。ただし、テストテンプレートが特定のステップのセットを指定して複数回実行するのに対して、動的テストは同じ呼び出しコンテキストを使用して異なるロジックを実行します。動的テストの用途の1つは、抽象オブジェクトのリストをストリーム化し、オブジェクトの具象型に応じて別個のアサーションのセットを実行する場合です。例についてはJUnit 5のドキュメントを参照してください

まとめ

JUnit 5はJUnitフレームワークの強力で柔軟なアップデート版です。JUnit 5には、テストケースの整理と説明に役立ち、テスト結果をわかりやすくするさまざまな改善と新機能があります。JUnit 5にアップデートするのは簡単で時間もかかりません。プロジェクトの使うライブラリを変更し、新しい機能を使い始めるだけです。

(この記事は、開発元Parasoft社 Blog 「Reaping the Benefits of Junit 5」2020年5月6日の翻訳記事です。)

JUnit 5 対応ツール Parasoft Jtestについて

Java対応静的解析・単体テストツール Parasoft Jtest

Jtestは、テスト工数の大幅削減とセキュアで高品質なJavaシステムの開発を強力にサポートするJava対応テストツールです。1,000個以上のコーディング規約をもとにソースコードを静的に解析し、プログラムの問題点や処理フローに潜む検出困難なエラーを検出します。さらに、JUnit(4、5共に対応)を用いた単体テストについて、作成、実行、テストカバレッジ分析、テスト資産の管理といった単体テストに係る作業をサポートし、単体テストの効率化を促進します。

Top