1. 概要

次世代のJUnitであるJUnit5 は、輝かしい新機能を備えた開発者テストの作成を容易にします。

そのような機能の1つに、 pパラメータ化テストがあります。 この機能により、異なるパラメータを使用して単一のテストメソッドを複数回実行できます。

このチュートリアルでは、パラメーター化されたテストについて詳しく説明するので、始めましょう。

2.2。 依存関係

JUnit 5パラメーター化テストを使用するには、junit-jupiter-paramsアーティファクトをJUnitプラットフォームからインポートする必要があります。 つまり、Mavenを使用する場合、pom.xmlに以下を追加します。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>

また、Gradleを使用する場合は、少し異なる方法で指定します。

testCompile("org.junit.jupiter:junit-jupiter-params:5.8.1")

3.3。 第一印象

既存のユーティリティ関数があり、その動作に自信を持っているとしましょう。

public class Numbers {
    public static boolean isOdd(int number) {
        return number % 2 != 0;
    }
}

パラメータ化されたテストは、 @ParameterizedTest アノテーションを追加することを除いて、他のテストと同じです。

@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
    assertTrue(Numbers.isOdd(number));
}

JUnit 5テストランナーは、上記のテストを6回実行します。その結果、isOddメソッドを6回実行します。 そして毎回、@ValueSource配列からnumberメソッドパラメーターに異なる値を割り当てます。

したがって、この例は、パラメーター化されたテストに必要な2つのことを示しています。

  • 引数のソース、この場合はint配列
  • それらにアクセスする方法、この場合はnumberパラメーター

この例では明らかではない別の側面があるので、これからも見ていきます。

4.4。 引数のソース

これまでに知っておくべきことですが、パラメーター化されたテストは、異なる引数を使用して同じテストを複数回実行します。

そして、うまくいけば、数字以上のことができるので、調べてみましょう。

4.1. 単純な値

@ValueSource アノテーションを使用すると、リテラル値の配列をテストメソッドに渡すことができます。

単純なisBlankメソッドをテストするとします。

public class Strings {
    public static boolean isBlank(String input) {
        return input == null || input.trim().isEmpty();
    }
}

このメソッドから、空白の文字列のnullに対してtrueが返されることが期待されます。 したがって、この動作を表明するためのパラメーター化されたテストを作成できます。

@ParameterizedTest
@ValueSource(strings = {"", "  "})
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) {
    assertTrue(Strings.isBlank(input));
}

ご覧のとおり、JUnitはこのテストを2回実行し、そのたびに配列からメソッドパラメーターに1つの引数を割り当てます。

バリューソースの制限の1つは、次のタイプのみをサポートすることです。

  • short shorts 属性付き)
  • byte bytes 属性)
  • int ints 属性)
  • long longs 属性)
  • float floats 属性)
  • double doubles 属性)
  • char chars 属性)
  • java.lang.String strings 属性)
  • java.lang.Class classes 属性)

また、毎回テストメソッドに渡すことができる引数は1つだけです。

先に進む前に、引数としてnullを渡していないことに注意してください。 これは別の制限です— 文字列とクラスであっても、@ValueSourceを介してnullを渡すことはできません。

4.2. ヌル値と空の値

JUnit 5.4以降、@ NullSource を使用して、パラメーター化されたテストメソッドに単一のnull値を渡すことができます。

@ParameterizedTest
@NullSource
void isBlank_ShouldReturnTrueForNullInputs(String input) {
    assertTrue(Strings.isBlank(input));
}

プリミティブデータ型はnull値を受け入れることができないため、プリミティブ引数に@NullSourceを使用することはできません。

同様に、@EmptySourceアノテーションを使用して空の値を渡すことができます。

@ParameterizedTest
@EmptySource
void isBlank_ShouldReturnTrueForEmptyStrings(String input) {
    assertTrue(Strings.isBlank(input));
}

@EmptySource は、注釈付きメソッドに1つの空の引数を渡します。

String 引数の場合、渡される値は空のStringと同じくらい単純です。 さらに、このパラメーターソースは、コレクションタイプと配列に空の値を提供できます。

null と空の値の両方を渡すために、合成された@NullAndEmptySourceアノテーションを使用できます。

@ParameterizedTest
@NullAndEmptySource
void isBlank_ShouldReturnTrueForNullAndEmptyStrings(String input) {
    assertTrue(Strings.isBlank(input));
}

と同じように @EmptySource 、作成された注釈は s、 コレクション s、および配列

パラメータ化されたテストにさらにいくつかの空の文字列バリエーションを渡すために、 @ ValueSource、@ NullSource、および@EmptySourceを組み合わせることができます

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"  ", "\t", "\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
    assertTrue(Strings.isBlank(input));
}

4.3. 列挙型

列挙型とは異なる値でテストを実行するために、@EnumSourceアノテーションを使用できます。

たとえば、すべての月の数値が1〜12であると断言できます。

@ParameterizedTest
@EnumSource(Month.class) // passing all 12 months
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {
    int monthNumber = month.getValue();
    assertTrue(monthNumber >= 1 && monthNumber <= 12);
}

または、 names 属性を使用して、数か月を除外することもできます。

また、4月、9月、6月、11月の長さは30日であるという事実を主張することもできます。

@ParameterizedTest
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
    final boolean isALeapYear = false;
    assertEquals(30, month.length(isALeapYear));
}

デフォルトでは、namesは一致した列挙値のみを保持します。

mode属性をEXCLUDEに設定することで、これを好転させることができます。

@ParameterizedTest
@EnumSource(
  value = Month.class,
  names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"},
  mode = EnumSource.Mode.EXCLUDE)
void exceptFourMonths_OthersAre31DaysLong(Month month) {
    final boolean isALeapYear = false;
    assertEquals(31, month.length(isALeapYear));
}

リテラル文字列に加えて、names属性に正規表現を渡すことができます。

@ParameterizedTest
@EnumSource(value = Month.class, names = ".+BER", mode = EnumSource.Mode.MATCH_ANY)
void fourMonths_AreEndingWithBer(Month month) {
    EnumSet<Month> months =
      EnumSet.of(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER, Month.DECEMBER);
    assertTrue(months.contains(month));
}

@ValueSource と非常によく似ており、 @EnumSource は、テストの実行ごとに1つの引数のみを渡す場合にのみ適用できます。

4.4. CSVリテラル

StringtoUpperCase()メソッドが期待される大文字の値を生成することを確認するとします。 @ValueSourceでは不十分です。

このようなシナリオのパラメーター化されたテストを作成するには、次のことを行う必要があります。

  • 入力値および期待値をテストメソッドに渡します
  • これらの入力値を使用して実際の結果を計算します
  • 主張する 期待値と実際の値

したがって、複数の引数を渡すことができる引数ソースが必要です。

@CsvSource は、それらのソースの1つです。

@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
    String actualValue = input.toUpperCase();
    assertEquals(expected, actualValue);
}

@CsvSource は、コンマ区切り値の配列を受け入れ、各配列エントリはCSVファイルの行に対応します。

このソースは、毎回1つの配列エントリを受け取り、それをコンマで分割し、各配列を個別のパラメーターとして注釈付きのテストメソッドに渡します。

デフォルトでは、コンマは列の区切り文字ですが、区切り文字属性を使用してカスタマイズできます。

@ParameterizedTest
@CsvSource(value = {"test:test", "tEst:test", "Java:java"}, delimiter = ':')
void toLowerCase_ShouldGenerateTheExpectedLowercaseValue(String input, String expected) {
    String actualValue = input.toLowerCase();
    assertEquals(expected, actualValue);
}

これでコロンで区切られた値になるため、CSVのままです。

4.5. CSVファイル

コード内でCSV値を渡す代わりに、実際のCSVファイルを参照できます。

たとえば、次のようなCSVファイルを使用できます。

input,expected
test,TEST
tEst,TEST
Java,JAVA

CSVファイルをロードし、ヘッダー列@CsvFileSourceで無視できます。

@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile(
  String input, String expected) {
    String actualValue = input.toUpperCase();
    assertEquals(expected, actualValue);
}

resources 属性は、読み取るクラスパス上のCSVファイルリソースを表します。 そして、それに複数のファイルを渡すことができます。

numLinesToSkip 属性は、CSVファイルを読み取るときにスキップする行数を表します。 デフォルトでは、@ CsvFileSourceは行をスキップしませんが、この機能は通常、ここで行ったようにヘッダー行をスキップするのに役立ちます。

単純な@CsvSourceと同様に、区切り文字は区切り文字属性を使用してカスタマイズできます。

列区切り文字に加えて、次の機能があります。

  • 行区切り文字は、 lineSeparator 属性を使用してカスタマイズできます—改行がデフォルト値です。
  • ファイルのエンコーディングは、 encoding 属性を使用してカスタマイズできます—UTF-8がデフォルト値です。

4.6. 方法

これまで取り上げてきた議論の出典はやや単純で、1つの制限があります。 それらを使用して複雑なオブジェクトを渡すことは困難または不可能です。

より複雑な引数を提供するための1つのアプローチは、引数のソースとしてメソッドを使用することです。

isBlankメソッドを@MethodSourceでテストしてみましょう。

@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
    assertEquals(expected, Strings.isBlank(input));
}

@MethodSource に指定する名前は、既存のメソッドと一致する必要があります。

それでは、次に ProvideStringsForIsBlank 引数のストリームを返す静的メソッドを記述しましょう。

private static Stream<Arguments> provideStringsForIsBlank() {
    return Stream.of(
      Arguments.of(null, true),
      Arguments.of("", true),
      Arguments.of("  ", true),
      Arguments.of("not blank", false)
    );
}

ここでは文字通り一連の引数を返していますが、これは厳密な要件ではありません。 例えば、 他のコレクションのようなインターフェースを返すことができますリスト。 

テスト呼び出しごとに1つの引数のみを提供する場合は、引数抽象化を使用する必要はありません。

@ParameterizedTest
@MethodSource // hmm, no method name ...
void isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument(String input) {
    assertTrue(Strings.isBlank(input));
}

private static Stream<String> isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument() {
    return Stream.of(null, "", "  ");
}

@MethodSource の名前を指定しない場合、JUnitはテストメソッドと同じ名前のソースメソッドを検索します。

異なるテストクラス間で引数を共有すると便利な場合があります。 このような場合、完全修飾名で現在のクラスの外部のソースメソッドを参照できます。

class StringsUnitTest {

    @ParameterizedTest
    @MethodSource("com.baeldung.parameterized.StringParams#blankStrings")
    void isBlank_ShouldReturnTrueForNullOrBlankStringsExternalSource(String input) {
        assertTrue(Strings.isBlank(input));
    }
}

public class StringParams {

    static Stream<String> blankStrings() {
        return Stream.of(null, "", "  ");
    }
}

FQN#methodName 形式を使用して、外部静的メソッドを参照できます。

4.7. カスタム引数プロバイダー

テスト引数に合格するためのもう1つの高度なアプローチは、ArgumentsProviderと呼ばれるインターフェイスのカスタム実装を使用することです。

class BlankStringsArgumentsProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
          Arguments.of((String) null), 
          Arguments.of(""), 
          Arguments.of("   ") 
        );
    }
}

次に、 @ArgumentsSource アノテーションを使用してテストにアノテーションを付け、このカスタムプロバイダーを使用できます。

@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {
    assertTrue(Strings.isBlank(input));
}

カスタムプロバイダーを、カスタムアノテーションで使用するためのより快適なAPIにしましょう。

4.8. カスタム注釈

静的変数からテスト引数をロードするとします。

static Stream<Arguments> arguments = Stream.of(
  Arguments.of(null, true), // null strings should be considered blank
  Arguments.of("", true),
  Arguments.of("  ", true),
  Arguments.of("not blank", false)
);

@ParameterizedTest
@VariableSource("arguments")
void isBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource(
  String input, boolean expected) {
    assertEquals(expected, Strings.isBlank(input));
}

実際、 JUnit 5はこれを提供していません。ただし、独自のソリューションを展開することはできます。

まず、注釈を作成できます。

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(VariableArgumentsProvider.class)
public @interface VariableSource {

    /**
     * The name of the static variable
     */
    String value();
}

次に、どういうわけかアノテーションの詳細を消費し、テスト引数を提供する必要があります。 JUnit 5は、これらを実現するために2つの抽象化を提供します。

  • AnnotationConsumerは注釈の詳細を消費します
  • ArgumentsProviderテスト引数を提供します

したがって、次に、 VariableArgumentsProvider クラスを指定された静的変数から読み取り、その値をテスト引数として返す必要があります。

class VariableArgumentsProvider 
  implements ArgumentsProvider, AnnotationConsumer<VariableSource> {

    private String variableName;

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return context.getTestClass()
                .map(this::getField)
                .map(this::getValue)
                .orElseThrow(() -> 
                  new IllegalArgumentException("Failed to load test arguments"));
    }

    @Override
    public void accept(VariableSource variableSource) {
        variableName = variableSource.value();
    }

    private Field getField(Class<?> clazz) {
        try {
            return clazz.getDeclaredField(variableName);
        } catch (Exception e) {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    private Stream<Arguments> getValue(Field field) {
        Object value = null;
        try {
            value = field.get(null);
        } catch (Exception ignored) {}

        return value == null ? null : (Stream<Arguments>) value;
    }
}

そしてそれは魅力のように機能します。

5.5。 引数の変換

5.1. 暗黙の変換

それらの@EnumTestの1つを@CsvSourceで書き直してみましょう。

@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Pssing strings
void someMonths_Are30DaysLongCsv(Month month) {
    final boolean isALeapYear = false;
    assertEquals(30, month.length(isALeapYear));
}

これは機能しないはずのようですが、どういうわけか機能します。

JUnit 5は、String引数を指定された列挙型に変換します。 このようなユースケースをサポートするために、JUnitJupiterには多数の組み込みの暗黙型コンバーターが用意されています。

変換プロセスは、各メソッドパラメータの宣言されたタイプによって異なります。 暗黙的な変換により、Stringインスタンスを次のようなタイプに変換できます。

  • UUID 
  • ロケール
  • LocalDate LocalTime LocalDateTime Year Monthなど。
  • ファイルおよびパス
  • URLおよびURI
  • 列挙型サブクラス

5.2. 明示的な変換

引数にカスタムの明示的なコンバーターを提供する必要がある場合があります。

yyyy / mm / dd 形式の文字列をLocalDateインスタンスに変換するとします。

まず、ArgumentConverterインターフェイスを実装する必要があります。

class SlashyDateConverter implements ArgumentConverter {

    @Override
    public Object convert(Object source, ParameterContext context)
      throws ArgumentConversionException {
        if (!(source instanceof String)) {
            throw new IllegalArgumentException(
              "The argument should be a string: " + source);
        }
        try {
            String[] parts = ((String) source).split("/");
            int year = Integer.parseInt(parts[0]);
            int month = Integer.parseInt(parts[1]);
            int day = Integer.parseInt(parts[2]);

            return LocalDate.of(year, month, day);
        } catch (Exception e) {
            throw new IllegalArgumentException("Failed to convert", e);
        }
    }
}

次に、@ConvertWithアノテーションを介してコンバーターを参照する必要があります。

@ParameterizedTest
@CsvSource({"2018/12/25,2018", "2019/02/11,2019"})
void getYear_ShouldWorkAsExpected(
  @ConvertWith(SlashyDateConverter.class) LocalDate date, int expected) {
    assertEquals(expected, date.getYear());
}

6.6。 引数アクセサー

デフォルトでは、パラメーター化されたテストに提供される各引数は、単一のメソッドパラメーターに対応します。 その結果、引数ソースを介して少数の引数を渡すと、テストメソッドのシグネチャが非常に大きくなり、乱雑になります。

この問題に対処する1つのアプローチは、渡されたすべての引数を ArgumentsAccessor のインスタンスにカプセル化し、インデックスとタイプで引数を取得することです。

Personクラスについて考えてみましょう。

class Person {

    String firstName;
    String middleName;
    String lastName;
    
    // constructor

    public String fullName() {
        if (middleName == null || middleName.trim().isEmpty()) {
            return String.format("%s %s", firstName, lastName);
        }

        return String.format("%s %s %s", firstName, middleName, lastName);
    }
}

fullName()メソッドをテストするために、 firstName middleName lastName 、およびの4つの引数を渡します。 fullNameが必要です。 ArgumentsAccessor を使用して、テスト引数をメソッドパラメーターとして宣言する代わりに取得できます。

@ParameterizedTest
@CsvSource({"Isaac,,Newton,Isaac Newton", "Charles,Robert,Darwin,Charles Robert Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(ArgumentsAccessor argumentsAccessor) {
    String firstName = argumentsAccessor.getString(0);
    String middleName = (String) argumentsAccessor.get(1);
    String lastName = argumentsAccessor.get(2, String.class);
    String expectedFullName = argumentsAccessor.getString(3);

    Person person = new Person(firstName, middleName, lastName);
    assertEquals(expectedFullName, person.fullName());
}

ここでは、渡されたすべての引数を ArgumentsAccessor インスタンスにカプセル化し、テストメソッド本体で、渡された各引数をそのインデックスとともに取得しています。 単なるアクセサーであることに加えて、型変換は get*メソッドによってサポートされます。

  • getString(index)は特定のインデックスの要素を取得し、それを Stringに変換します—同じことがプリミティブタイプにも当てはまります。
  • get(index)は、特定のインデックスにある要素をObjectとして取得するだけです。
  • get(index、type)は特定のインデックスの要素を取得し、それを指定されたtypeに変換します。

7。 引数アグリゲーター

ArgumentsAccessor 抽象化を直接使用すると、テストコードが読みにくくなったり再利用できなくなったりする可能性があります。 これらの問題に対処するために、カスタムで再利用可能なアグリゲーターを作成できます。

これを行うには、ArgumentsAggregatorインターフェイスを実装します。

class PersonAggregator implements ArgumentsAggregator {

    @Override
    public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
      throws ArgumentsAggregationException {
        return new Person(
          accessor.getString(1), accessor.getString(2), accessor.getString(3));
    }
}

次に、@AggregateWithアノテーションを介して参照します。

@ParameterizedTest
@CsvSource({"Isaac Newton,Isaac,,Newton", "Charles Robert Darwin,Charles,Robert,Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(
  String expectedFullName,
  @AggregateWith(PersonAggregator.class) Person person) {

    assertEquals(expectedFullName, person.fullName());
}

PersonAggregator は、最後の3つの引数を取り、それらからPersonクラスをインスタンス化します。

8.8。 表示名のカスタマイズ

デフォルトでは、パラメーター化されたテストの表示名には、渡されたすべての引数のString表現とともに呼び出しインデックスが含まれます。

├─ someMonths_Are30DaysLongCsv(Month)
│     │  ├─ [1] APRIL
│     │  ├─ [2] JUNE
│     │  ├─ [3] SEPTEMBER
│     │  └─ [4] NOVEMBER

ただし、@ParameterizedTestアノテーションのname属性を使用して、この表示をカスタマイズできます。

@ParameterizedTest(name = "{index} {0} is 30 days long")
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
    final boolean isALeapYear = false;
    assertEquals(30, month.length(isALeapYear));
}

4月の長さは30日です確かに、より読みやすい表示名です。

├─ someMonths_Are30DaysLong(Month)
│     │  ├─ 1 APRIL is 30 days long
│     │  ├─ 2 JUNE is 30 days long
│     │  ├─ 3 SEPTEMBER is 30 days long
│     │  └─ 4 NOVEMBER is 30 days long

表示名をカスタマイズする場合は、次のプレースホルダーを使用できます。

  • {index}は呼び出しインデックスに置き換えられます。 簡単に言うと、最初の実行の呼び出しインデックスは1、2番目の実行の呼び出しインデックスは2というように続きます。
  • {arguments} は、完全なコンマ区切りの引数リストのプレースホルダーです。
  • {0}、{1}、… は、個々の引数のプレースホルダーです。

9. 結論

この記事では、JUnit5でのパラメーター化されたテストの要点を探りました。

パラメータ化されたテストは、2つの点で通常のテストとは異なることを学びました。つまり、 @ParameterizedTest でアノテーションが付けられ、宣言された引数のソースが必要です。

また、これまでに、JUnitが引数をカスタムターゲットタイプに変換したり、テスト名をカスタマイズしたりするためのいくつかの機能を提供していることを知っておく必要があります。

いつものように、サンプルコードは GitHub プロジェクトで入手できるので、必ずチェックしてください。