JUnit 5パラメータ化テストのガイド

  • link:/category/testing/ [テスト]

  • JUnit 5

1. 概要

次世代のJUnitであるlink:/junit-5[JUnit 5]は、新しい機能を備えた開発者テストの作成を容易にします。
そのような機能の1つに_parameterized tests._があります。この機能により、*異なるパラメーターを使用して1つのテストメソッドを複数回実行できます。*
このチュートリアルでは、パラメータ化されたテストを詳細に検討するため、始めましょう!

2。 依存関係

JUnit 5パラメータ化テストを使用するには、https://search.maven.org/search?q=a:junit-jupiter-params%20AND%20g:org.junit.jupiter[_junit-jupiter- params_] JUnitプラットフォームからのアーティファクト。 つまり、Mavenを使用する場合、_pom.xml_に次を追加します。
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.4.2</version>
    <scope>test</scope>
</dependency>
また、Gradleを使用する場合は、少し異なる方法で指定します。
testCompile("org.junit.jupiter:junit-jupiter-params:5.4.2")

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テストランナーは上記のテストを実行し、その結果、__isOdd __methodを6回実行します。 そして、毎回、_ @ ValueSource_配列の異なる値を_number_メソッドパラメーターに割り当てます。
したがって、この例は、パラメーター化されたテストに必要な2つのことを示しています。
  • 引数のソース、_ int_配列、この場合

  • それらにアクセスする方法、この場合は、_number_パラメーター

    また、この例では明らかではないことがもう1つありますので、ご期待ください。

4。 引数ソース

これでわかるはずですが、パラメータ化されたテストは、同じテストを異なる引数で複数回実行します。
そして、うまくいけば、数字以上のことができるようになるので、探検しましょう!

4.1. 単純な値

  • @ ValueSource _annotationを使用すると、リテラル値の配列をテストメソッドに渡すことができます*。

    たとえば、単純な_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 _(_string_属性付き)

  • java.lang.Class(classes_属性付き)

    また、*テストメソッドに毎回渡すことができる引数は1つだけです*。
    さらに先に進む前に、_null_を引数として渡していないことに気付きましたか? それは別の制限です:* _String_と_Class_であっても_null_をa _ @ ValueSourceに渡すことはできません!*

4.2. ヌルおよび空の値

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

@ParameterizedTest
@NullSource
void isBlank_ShouldReturnTrueForNullInputs(String input) {
    assertTrue(Strings.isBlank(input));
}
プリミティブデータ型は__null __valuesを受け入れることができないため、プリミティブ引数に__ @ NullSource __を使用することはできません。
ほぼ同様に、___ @ EmptySource ___annotationを使用して空の値を渡すことができます。
@ParameterizedTest
@EmptySource
void isBlank_ShouldReturnTrueForEmptyStrings(String input) {
    assertTrue(Strings.isBlank(input));
}
  • @ EmptySource _は、注釈付きメソッドに単一の空の引数を渡します*。

    _String_引数の場合、渡される値は空の_String_と同じくらい単純です。 *さらに、このパラメーターソースは、_Collection_型および配列に空の値を提供できます*。
    __null __とemptyの両方の値を渡すために、composed __ @ NullAndEmptySource __annotationを使用できます。
@ParameterizedTest
@NullAndEmptySource
void isBlank_ShouldReturnTrueForNullAndEmptyStrings(String input) {
    assertTrue(Strings.isBlank(input));
}
_ @ EmptySource_と同様に、合成された注釈は__String__s _、_ __Collection__s _、_およびarray __.__に対して機能します
いくつかの空の文字列バリエーションをパラメーター化されたテストに渡すために、* * __ @ ValueSource、@ NullSource、@ EmptySource __togetherを組み合わせることができます:*
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"  ", "\t", "\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
    assertTrue(Strings.isBlank(input));
}

4.3. Enum

*列挙とは異なる値でテストを実行するには、_ @ 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 ___attributeを使用して、数か月を除外できます。
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_ attribute __:__に正規表現を渡すことができます
@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リテラル

_String_からの_toUpperCase()_メソッドが期待される大文字の値を生成することを確認しようとしているとします。 __ @ ValueSource __では不十分です。
このようなシナリオのパラメーター化されたテストを作成するには、次の手順を実行する必要があります。
  • 入力値_ and an *期待値*をテストメソッドに渡します

  • これらの入力値で*実際の結果を計算します*

  • アサート実際の値と期待値

    したがって、複数の引数を渡すことができる引数ソースが必要です。 _ @ 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つの配列エントリを取り、コンマで分割し、各配列を個別のパラメーターとして注釈付きのテストメソッドに渡します。 デフォルトでは、コンマは列の区切り文字ですが、_delimiter_属性を使用してカスタマイズできます。
@ParameterizedTest
@CsvSource(value = {"test:test", "tEst:test", "Java:java"}, delimiter = ':')
void toLowerCase_ShouldGenerateTheExpectedLowercaseValue(String input, String expected) {
    String actualValue = input.toLowerCase();
    assertEquals(expected, actualValue);
}
これは_colon_で区切られた値ですが、まだ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 __attributeは、読み取るクラスパス上のCSVファイルリソースを表します。 そして、複数のファイルをそれに渡すことができます。
__numLinesToSkip __attributeは、CSVファイルの読み取り時にスキップする行数を表します。 *デフォルトでは、__ @ CsvFileSource ___は行をスキップしませんが、この機能は通常ここで行ったようにヘッダー行をスキップするのに便利です*。
simple _ @ CsvSource_と同様に、区切り文字はthe__delimiter ___attributeでカスタマイズできます。
列区切り記号に加えて:
  • 行区切りは、_lineSeparator_を使用してカスタマイズできます。
    属性-改行がデフォルト値です

  • ファイルのエンコードは、_encoding_属性を使用してカスタマイズできます–
    UTF-8がデフォルト値です

4.6. 方法

これまで説明してきた引数のソースは、いくぶん単純であり、1つの制限があります。それらを使用して複雑なオブジェクトを渡すことは困難または不可能です。
*より複雑な引数を提供する1つの方法は、引数ソースとしてメソッドを使用することです*。
a _ @ MethodSource:_で__isBlank __methodをテストしましょう
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
    assertEquals(expected, Strings.isBlank(input));
}
_ @ MethodSource_に指定する名前は、既存のメソッドと一致する必要があります。
次に、__ Argument__s *の_Stream_を返す_provideStringsForIsBlank _、* a __static __methodを作成してみましょう。
private static Stream<Arguments> provideStringsForIsBlank() {
    return Stream.of(
      Arguments.of(null, true),
      Arguments.of("", true),
      Arguments.of("  ", true),
      Arguments.of("not blank", false)
    );
}
ここでは文字通り引数のストリームを返していますが、それは厳密な要件ではありません。 たとえば、****** __ * List。* __などの他のコレクションのようなインターフェイスを返すことができます
テスト呼び出しごとに引数を1つだけ提供する場合、__Arguments ___abstractionを使用する必要はありません。
@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. カスタム引数プロバイダー

テスト引数を渡す別の高度なアプローチは、_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 __annotationを使用してテストに注釈を付け、このカスタムプロバイダーを使用できます。
@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つのことを実現するために2つの抽象化を提供します。
  • AnnotationConsumer _は注釈の詳細を使用します

  • ArgumentsProvider はテスト引数を提供します

    そのため、次に、__VariableArgumentsProvider __classを指定された静的変数から読み取り、その値をテスト引数として返す必要があります。
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.1. 暗黙的な変換

@_CsvSource:_でこれらの__ @ EnumTest__sの1つを書き直しましょう。
@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 ___argumentsを指定された列挙型に変換します。 このようなユースケースをサポートするために、JUnit Jupiterには多数の組み込みの暗黙的なタイプコンバーターが用意されています。
変換プロセスは、各メソッドパラメータの宣言されたタイプに依存します。 暗黙的な変換では、_String_インスタンスを次のような型に変換できます。
  • _UUID _

  • ロケール

  • LocalDate、LocalTime、LocalDateTime、年、月など

  • _ファイル_およびpath

  • URL and URI

  • Enum subclasses

5.2. 明示的な変換

引数のカスタムおよび明示的なコンバーターを提供する必要がある場合があります。
__yyyy / mm / dd ___formatの文字列を_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 ___annotationを使用してコンバーターを参照する必要があります。
@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。 引数アクセサー

デフォルトでは、パラメーター化されたテストに提供される各引数は、単一のメソッドパラメーターに対応します。 そのため、引数ソースを介して少数の引数を渡すと、テストメソッドのシグネチャが非常に大きくなり煩雑になります。
この問題に対処する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、_、__expected fullNameの4つの引数を渡します。 __テスト引数をメソッドパラメーターとして宣言する代わりに、__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());
}
ここでは、渡されたすべての引数をan__ArgumentsAccessor __instanceにカプセル化してから、テストメソッド本体で、渡された各引数をそのインデックスで取得しています。 単なるアクセサであることに加えて、_get * _メソッドを通じて型変換がサポートされています。
  • getString(index)は、特定のインデックスの要素を取得し、
    _String
    theに変換します。プリミティブ型についても同様です。

  • get(index)simplyは、特定のインデックスの要素を次のように取得します
    an Object

  • get(index、type)_は、特定のインデックスの要素を取得し、
    指定された_type_に変換します

7。 引数アグリゲーター

__ArgumentsAccessor __abstractionを直接使用すると、テストコードの可読性または再利用性が低下する場合があります。 これらの問題に対処するために、カスタムで再利用可能なアグリゲーターを作成できます。
それを行うには、__ArgumentsAggregator ___interfaceを実装します。
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 __annotationを介して参照します。
@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 ___classをインスタンス化します。

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

デフォルトでは、パラメータ化されたテストの表示名には、次のようなすべての渡された引数の__String __representationとともに呼び出しインデックスが含まれます。
├─ 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. 結論

この記事では、JUnit 5のパラメーター化されたテストの要点について説明しました。
パラメーター化されたテストは、2つの面で通常のテストとは異なることを学びました。they_ @ ParameterizedTest_で注釈が付けられ、宣言された引数のソースが必要です。
また、JUnitには、引数をカスタムターゲットタイプに変換したり、テスト名をカスタマイズしたりするための機能が用意されているはずです。
いつものように、サンプルコードはhttps://github.com/eugenp/tutorials/tree/master/testing-modules/junit-5[GitHub]プロジェクトで入手できます。必ずチェックしてください。