X

A blog about Oracle Technology Network Japan

  • August 26, 2020

JUnit 4からJUnit 5に移行する:重要な違いと利点

Guest Author

※本記事は、Brian McGlauflinによる"Migrating from JUnit 4 to JUnit 5: Important Differences and Benefits"を翻訳したものです。


改善点と新機能が魅力のJUnit 5

著者:Brian McGlauflin

2020年4月6日

 

JUnit 5は、JUnitフレームワークの強力かつ柔軟なアップデートであり、テスト・ケースの整理および記述を行うためや、テスト結果の理解に役立てるためのさまざまな改善と新機能が提供されています。JUnit 5へのアップデートは、すばやく簡単です。プロジェクトの依存性を更新し、新機能を使い始めるだけです。

しばらくJUnit 4を使っていた方は、テストの移行を大変な作業だと感じるかもしれません。朗報なのは、おそらくテストの変換がまったく必要ないという点です。JUnit 5ではVintageライブラリを使ってJUnit 4テストを実行できます。

とは言うものの、新しいテストをJUnit 5で書き始めるべき4つの確かな理由を次に示します。

  • JUnit 5では、ラムダ関数などのJava 8以降の機能を活用することで、テストが強力になるとともに、メンテナンスしやすくなっています。
  • JUnit 5には、テストの記述、整理、実行にとても便利ないくつかの新機能が追加されています。たとえば、テストの表示名がわかりやすくなり、テストを階層的に整理できるようになっています。
  • JUnit 5は複数のライブラリで構成されているため、必要な機能だけがプロジェクトにインポートされます。MavenやGradleなどのビルド・システムを使えば、適切なライブラリを含めるのは簡単です。
  • JUnit 5では、複数の拡張機能を同時に使用できます。これはJUnit 4では不可能でした(一度に使えるランナーは1つだけでした)。つまり、Spring拡張機能と他の拡張機能(独自のカスタム拡張機能など)を容易に組み合わせることができます。

JUnit 4からJUnit 5への切り替えはとても簡単です。この点は、既存のJUnit 4テストがある場合でも変わりません。ほとんどの組織では、新機能が必要な場合を除き、古いJUnitテストをJUnit 5に変換する必要はありません。新機能が必要な場合は、以下の手順に従います。

  1. ライブラリとビルド・システムをJUnit 4からJUnit 5にアップデートします。既存のテストを実行できるように、テスト・ランタイム・パスにjunit-vintage-engineアーティファクトを含めます。
  2. 新しいJUnit 5コンストラクトを使って、新しいテストの構築を始めます。
  3. (省略可能)JUnitテストをJUnit 5に変換します。

 

重要な違い

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")となっています。
  • ほとんどのアサーションで、エラー・メッセージを構築するラムダを受け取るようになっています。このラムダが呼ばれるのは、アサーションに失敗した場合のみです。
  • assertTimeout()とassertTimeoutPreemptively()は、@Timeoutアノテーションに代わるものです(JUnit 5にも@Timeoutアノテーションは存在しますが、JUnit 4とは動作が異なります)。
  • 後述しますが、いくつかの新しいアサーションが追加されています。

なお、お好みであればJUnit 5テストでJUnit 4のアサーションを継続して使用できることに注意してください。

前提条件:前提条件はorg.junit.jupiter.api.Assumptionsに移動しました。 

同じ前提条件が存在していますが、BooleanSupplierや、条件との照合を行うHamcrest Matcherもサポートされるようになっています。条件を満たした場合のコード実行に、ラムダ(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では、拡張機能を使ってこの点が改善され、シンプルになっています。

たとえば、Springフレームワークを使ってテストを構築する場合、JUnit 4では次のようにしていました。

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

JUnit 5では、次のようにしてSpring拡張機能をインクルードします。

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

@ExtendWithアノテーションは繰り返し可能であるため、複数の拡張機能を容易に組み合わせることができます。

org.junit.jupiter.api.extensionで定義されたインタフェースを1つまたは複数実装したクラスを作成し、@ExtendWithを使ってそのクラスをテストに追加して、独自のカスタム拡張機能を容易に定義することもできます。

 

テストをJUnit 5に変換する

既存の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アノテーションで置き換えます。その際のサンプルとして、使用している拡張機能に関する更新版ドキュメントをオンラインで探すとよいでしょう。

なお、パラメータ化テストを移行する場合は、もう少しリファクタリングが必要になることに注意してください。Unit 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()では、複数のアサーションをグループ化します。個々のアサーションが失敗しても、すべてのアサーションが実行されるという利点がもたらされます。
  • assertThrows()とassertDoesNotThrow()は、@Testアノテーションのexpectedプロパティに代わるものです。

テストのネスト:JUnit 4のテスト・スイートは便利でしたが、JUnit 5の「テストのネスト」の方がセットアップやメンテナンスが簡単です。同時に、テスト・グループ間のリレーションシップの表現力が向上しています。次に例を示します。

@DisplayName("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 with initialization(初期化時にMyClassを検証する))と、テストで検証する内容(myMethod returns true(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を追加することを示します(両方を使う場合は、上記のように組み合わせて使うこともできます)。@EnumSourceや@ArgumentsSource(カスタムの値プロバイダ)など、値のソースは他にも複数あります。複数のパラメータが必要な場合は、@MethodSourceや@CsvSourceを使うこともできます。

JUnit 5には、@RepeatedTestというテスト・タイプも追加されました。このテスト・タイプは、1つのテストを指定された回数だけ繰り返すものです。

条件付きテスト実行:JUnit 5では、テストまたはコンテナ(テスト・クラス)を条件によって有効化または無効化するExecutionCondition拡張機能APIが提供されています。これはテストで@Disabledを使うことに似ていますが、カスタムの条件を定義できます。以下のような複数の組込み条件があります。

  • @EnabledOnOs/@DisabledOnOs:指定されたオペレーティング・システムでのみ、テストを有効化または無効化する
  • @EnabledOnJre/@DisabledOnJre:特定のバージョンのJavaでテストを有効化または無効化することを指定する
  • @EnabledIfSystemProperty:JVMシステム・プロパティの値に基づいてテストを有効化する
  • @EnabledIf:スクリプトのロジックで記述された条件が満たされた場合にテストを有効化する

テスト・テンプレート:テスト・テンプレートは、通常のテストとは異なり、実行する一連の手順を定義するものです。このテンプレートは、特定の起動コンテキストを使って他の場所から実行することができます。つまり、テスト・テンプレートをいったん定義してから、そのテストを行うために使用する、実行時の起動コンテキストのリストを作成することができます。詳細および例については、ドキュメントをご覧ください

動的テスト:動的テストは、テスト・テンプレートに似ており、行うテストがその実行時に生成されます。ただし、テスト・テンプレートは特定の一連の手順を複数回実行するように定義されるのに対し、動的テストでは同じ起動コンテキストを使用して別のロジックを実行することができます。動的テストの使用方法の1つとして、抽象オブジェクトのリストをストリーミングし、それぞれの具象型に基づいて別々のアサーション・セットを実行することが考えられます。ドキュメントには、見本となる例が記載されています

 

まとめ

JUnit 5の新機能を使いたい場合を除けば、古いJUnit 4テストをJUnit 5に変換する必要はおそらくないでしょう。しかし、JUnit 5に切り替えるべき大きな理由があります。その1つとして、JUnit 5のテストは強力になるとともに、メンテナンスしやすくなっていることが挙げられます。さらに、JUnit 5には便利な機能が数多く搭載されています。使う機能だけがインポートされることに加え、複数の拡張機能を使うことや、独自のカスタム拡張機能を作成することもできます。こういった変更点や新機能が組み合わされて、強力で柔軟な、JUnitフレームワークのアップデートが実現しています。


Brian McGlauflin

Parasoftのソフトウェア・エンジニア。SpringやAndroid、APIテスト、サービス仮想化を活用したフル・スタック開発を経験。現在は、Parasoft Jtestを使った、Javaアプリケーションの自動ソフトウェア・テストに重点的に取り組んでいる。

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.