JUnitでパラメータ化テストをすばやく作成する方法

パラメータ化テストは、データだけが異なる複数のテストケースを定義して実行するのによい方法です。ここでは、JUnitテストでよく使用される3つのフレームワークについて説明します。

ユニットテストを書くときは、メソッドの入力パラメーターと期待される結果をテストメソッド自体で初期化するのが一般的です。場合によっては、少数の入力の組み合わせだけで十分かもしれません。しかし、コードのすべての機能を検証するために膨大な値の組み合わせを使用しなければならない場合もあります。パラメータ化テストは、データだけが異なる複数のテストケースを定義して実行するのによい方法です。境界ケースを含むさまざまな値でコードの動作を検証できます。テストをパラメータ化すると、コードカバレッジが向上し、コードが期待どおりに機能しているという確信が得られます。

Javaには優れたパラメータ化フレームワークがいくつもあります。この記事では、JUnitテストでよく使用される3つのフレームワークを比較し、それぞれのテストがどのように構成されているかの例を示します。最後に、パラメータ化されたテストを簡単にすばやく作成する方法を説明します。

パラメータ化JUnitテストフレームワーク

最も一般的な3つのフレームワークとしてJUnit 4、JunitParams、JUnit 5を比較してみましょう。各JUnitパラメータ化フレームワークには、それぞれ長所と短所があります。

JUnit 4

長所:

  • JUnit 4に組み込まれているパラメータ化フレームワークであるため、追加の外部ライブラリは必要ありません。
  • 古いバージョンのJava(JDK 7以前)をサポートしています。

短所:

  • テストクラスはフィールドやコンストラクターを使用してパラメーターを定義するので、テストがより冗長になります。
  • テスト対象のメソッドごとに別のテストクラスが必要です。

JunitParams

長所:

  • パラメーターをテストメソッドに直接渡せるため、パラメーター構文が簡潔です。
  • テストクラスごとに複数のテストメソッド(それぞれ独自のデータを持つ)を使用できます。
  • CSVデータソースやアノテーションベースの値(メソッドは必要ありません)をサポートします。

短所:

  • プロジェクトにJunitParamsライブラリへの依存関係を設定する必要があります。
  • テストの実行とデバッグでは、クラス内のすべてのテストを実行する必要があります。テストクラス内の単独のテストメソッドを実行することはできません。

JUnit 5

長所:

  • JUnit 5に組み込まれたパラメータ化フレームワークであり、JUnit 4のフレームワークを改善したものです。
  • JunitParamsのような簡潔なパラメーター構文を持っています。
  • CSVやアノテーション(メソッドは必要ありません)など複数のタイプのデータセットソースをサポートします。
  • 追加のライブラリは必要ありませんが、複数の.jarが必要です。

短所:

  • Java 8以降のビルドシステム(Gradle 4.6またはMaven Surefire 2.21)が必要です。
  • 使用しているIDEではまだサポートされていない可能性があります(この記事の執筆時点では、EclipseとIntelliJだけがJUnit 5をサポートしています)。

サンプル

たとえば、銀行のローン申し込みを処理するメソッドがあるとします。すると、ローン希望額、頭金、その他の値を指定するユニットテストを書くことになるでしょう。次に、応答を検証するアサーションを作成します。ローンは承認または却下され、応答によってローンの条件が指定されます。

例:

public LoanResponse requestLoan(float loanAmount, float downPayment, float availableFunds)
{
    LoanResponse response = new LoanResponse();
    response.setApproved(true);

    if (availableFunds < downPayment) {
        response.setApproved(false);
        response.setMessage("error.insufficient.funds.for.down.payment");
        return response;
    }
    if (downPayment / loanAmount < 0.1) {
        response.setApproved(false);
        response.setMessage("error.insufficient.down.payment");
    }

    return response;
}

まず、上記のメソッドの通常のテストを見てみましょう。

@Test
    public void testRequestLoan() throws Throwable
    {
        // Given
        LoanProcessor underTest = new LoanProcessor();

        // When
        LoanResponse result = underTest.requestLoan(1000f, 200f, 250f);

        // Then
        assertNotNull(result);
        assertTrue(result.isApproved());
        assertNull(result.getMessage());
    }

この例では、頭金200ドルで1000ドルのローンを申請し、申請者が250ドルの手元資金を持っていることを示してメソッドをテストしています。このテストでは、ローンが承認され、応答にメッセージが表示されなかったことを検証しています。

requestLoan()メソッドが完全にテストされていることを確認するために、さまざまな頭金、ローン希望額、資金でテストする必要があります。たとえば、頭金なしの100万ドルのローン申し込みをテストしましょう。これは却下されるはずです。単純に既存のテストを複製して別の値を指定することもできますが、テストロジックが同じであるため、テストをパラメータ化する方が効率的です。

ローン申し込み額、頭金、手元資金、そして予想される結果(ローンが承認されるかどうか、および検証後に返されるメッセージ)をパラメータ化します。リクエストデータの各セットは、期待される結果と合わせて、個別のテストケースになります。

JUnit 4を使用したパラメータ化テストの例

まず、JUnit 4 でのパラメータ化のサンプルから始めましょう。パラメータ化テストを作成するには、まずテスト用の変数を定義する必要があります。また、変数を初期化するコンストラクターも含める必要があります。

@RunWith(Parameterized.class)
public class LoanProcessorParameterizedTest {

    float loanAmount;
    float downPayment;
    float availableFunds;
    boolean expectApproved;
    String expectedMessage;

    public LoanProcessorParameterizedTest(float loanAmount, float downPayment,
        float availableFunds, boolean expectApproved, String expectedMessage)
    {
        this.loanAmount = loanAmount;
        this.downPayment = downPayment;
        this.availableFunds = availableFunds;
        this.expectApproved = expectApproved;
        this.expectedMessage = expectedMessage;
    }
    // ...
}

ここでは、テストで@RunWithアノテーションを使用して、JUnit4 の Parameterized ランナーでテストを実行するよう指定しています。このランナーは、テスト用の値セットを提供するメソッド( @Parametersアノテーションによって示される)を探し、テストを適切に初期化し、複数のデータ行を使ってテストを実行します。

各パラメーターはテストクラスのフィールドとして定義され、コンストラクターがこれらの値を初期化します(コンストラクターを作成せずに、 @Parameterアノテーションを使用してフィールドに値を注入することもできます)。値セットの各行について、 Parameterizedランナーはテストクラスをインスタンス化し、クラス内の各テストを実行します。

Parameterizedランナーにパラメーターを提供するメソッドを追加してみます。

@Parameters(name = "Run {index}: loanAmount={0}, downPayment={1}, availableFunds={2}, expectApproved={3}, expectedMessage={4}")
    public static Iterable<Object[]> data() throws Throwable
    {
        return Arrays.asList(new Object[][] {
            { 1000.0f, 200.0f, 250.0f, true, null }
        });
    }

値セットは、 @Parametersアノテーションが付いたdata()メソッドによってObjec配列のListとして構築されます。@Parametersはプレースホルダーを使用してテストの名前を設定します。プレースホルダーはテストの実行時に置き換えられます。これにより、後で説明するように、テスト結果の値が見やすくなります。現在、ローンが承認されるケースをテストするデータ行しかありません。より多くの行を追加して、テスト対象メソッドのカバレッジを上げることができます。

@Parameters(name = "Run {index}: loanAmount={0}, downPayment={1}, availableFunds={2}, expectApproved={3}, expectedMessage={4}")
    public static Iterable<Object[]> data() throws Throwable
    {
        return Arrays.asList(new Object[][] {
            { 1000.0f, 200.0f, 250.0f,  true, null },
            { 1000.0f,  50.0f, 250.0f, false, "error.insufficient.down.payment" },
            { 1000.0f, 200.0f, 150.0f, false, "error.insufficient.funds.for.down.payment" }
        });
    }

これで、ローンが承認されるテストケースが1つと、別々の理由で却下されるケースが2つできました。さらに、ゼロまたは負の値が使用される行や、テスト境界条件を追加するとよいでしょう。

いよいよテストメソッドを作成する準備が整いました。

@Test
    public void testRequestLoan() throws Throwable
    {
        // Given
        LoanProcessor underTest = new LoanProcessor();

        // When
        LoanResponse result = underTest.requestLoan(loanAmount, downPayment, availableFunds);

        // Then
        assertNotNull(result);
        assertEquals(expectApproved, result.isApproved());
        assertEquals(expectedMessage, result.getMessage());
    }

requestLoan()メソッドを呼び出すときと結果を検証するときにフィールドを参照しています。

JunitParamsの例

JunitParamsライブラリは、パラメーターをテストメソッドに直接渡せるため、パラメーター構文が簡潔です。パラメーター値は、@Parametersアノテーションで参照される別のメソッドによって提供されます。

@RunWith(JUnitParamsRunner.class)
public class LoanProcessorParameterizedTest2 {

    @Test
    @Parameters(method = "testRequestLoan_Parameters")
    public void testRequestLoan(float loanAmount, float downPayment, float availableFunds,
        boolean expectApproved, String expectedMessage) throws Throwable
    {
        ...
    }

    @SuppressWarnings("unused")
    private static Object[][] testRequestLoan_Parameters() throws Throwable {
        // Parameters: loanAmount={0}, downPayment={1}, availableFunds={2}, expectApproved={3}, expectedMessage={4}
        return new Object[][] {
            { 1000.0f, 200.0f, 250.0f,  true, null },
            { 1000.0f,  50.0f, 250.0f, false, "error.insufficient.down.payment"},
            { 1000.0f, 200.0f, 150.0f, false, "error.insufficient.funds.for.down.payment" }
        };

    }
}

JunitParamsには、コード内で値を指定する方法のほかに、CSVファイルを使用して値を提供できるという利点もあります。これにより、テストとデータが分離されるので、コードを更新せずにデータ値を更新することができます。

JUnit 5の例

JUnit 5は、JUnit 4の限界と欠点に対処しています。JunitParamsと同様に、JUnit 5のパラメータ化テスト構文は簡素です。構文の最も重要な変更点は次のとおりです。

  • テストメソッドには@Testではなく@ParameterizedTestというアノテーションが付けられます。
  • テストメソッドは、フィールドとコンストラクターを使用する代わりに、直接パラメーターを受け取ります。
  • @RunWithアノテーションは不要になりました。

同じサンプルをJUnit 5で定義すると、次のようになります。

public class LoanProcessorParameterizedTest {

    @ParameterizedTest(name="Run {index}: loanAmount={0}, downPayment={1}, availableFunds={2}, expectApproved={3}, expectedMessage={4}")
    @MethodSource("testRequestLoan_Parameters")
    public void testRequestLoan(float loanAmount, float downPayment, float availableFunds,
            boolean expectApproved, String expectedMessage) throws Throwable
    {
        ...
    }

    static Stream<Arguments> testRequestLoan_Parameters() throws Throwable {
        return Stream.of(
            Arguments.of(1000.0f, 200.0f, 250.0f,  true, null),
            Arguments.of(1000.0f,  50.0f, 250.0f, false, "error.insufficient.down.payment"),
            Arguments.of(1000.0f, 200.0f, 150.0f, false, "error.insufficient.funds.for.down.payment")
        );
    }
}

パラメータ化テストを効率的に作成する

ご想像どおり、上記のようなパラメータ化テストを書くのは一仕事です。どのパラメータ化テストフレームワークにも、間違いなく記述しなければならない定型コードが多少なりともあります。正しい構文を覚えるのは難しいかもしれませんし、書きあげるのには時間がかかります。これをはるかに簡単にする方法として、 Parasoft Jtestを使用して、上記のようなパラメータ化テストを自動的に生成することができます。それには、EclipseまたはIntelliJでテストを生成したいメソッドを選択し、Parasoft JtestのUnit Test Assistantビューで[パラメータライズ]アクションをクリックします。

デフォルト値とアサーションを使用してテストが生成されます。次に、テストに実際の入力値とアサーションを設定し、 data()メソッドにデータ行を追加します。

パラメータ化テストの実行

Parasoft JtestはEclipseとIntelliJの両方で直接パラメータ化テストを実行できます。

EclipseのJUnitビュー

上の図で示されているように、各テストの名前には、データセットの入力値と期待される結果値が含まれています。これにより、それぞれのケースについて入力パラメーターと期待される出力が表示されるため、テストが失敗したとき、テストのデバッグが非常に簡単です。

Parasoft Jtestの[すべて実行]アクションを使用することもできます。

Parasoft Jtestのフローツリービュー

テストフローを分析し、直前のテスト実行に関する詳細情報を提供します。これにより、ブレークポイントやデバッグステートメントを使ってテストを再実行しなくても、テストで何が起きたかを確認することができます。たとえば、変数ビューにはパラメータ化された値が表示されます。

Parasoft Jtestの変数ビュー

まとめ

ここでレビューした3つのフレームワークは、どれも良い選択肢であり、うまく機能します。JUnit 4を使用している場合は、私ならJUnit 4 ParameterizedフレームワークよりもJunitParamsを選びます。これは、テストクラスの設計が明快で、同じクラスに複数のテストメソッドを定義できるからです。ただし、JUnit 5を使用している場合は、JUnit 4の欠点に対処し、追加のライブラリを必要としない組み込みのJUnit 5フレームワークをお勧めします。Parasoft Jtestのユニットテスト機能を使用して、パラメータ化テストの作成、実行、デバッグをより効率的にするのも良い考えです。

(この記事は、開発元Parasoft社 Blog 「How to Expedite the Creation of JUnit Parameterized Tests」2018年9月19日の翻訳記事です。)

Top