JUnit 5 のパラメーター化テストは超便利


JUnit 5 といえば @Nested テストが一押しかなと思っていた時期もありましたが、 @ParameterizedTest を使い始めたら「JUnit 4 のあれは何だったんだ」と思えるくらい手になじんでとてもいい感じです。これだけでも移行をオススメできます。

確認環境

  • JUnit 5.3
  • AdoptOpenJDK 11.0.3+7
  • macOS 10.14.3

ValueSource

パラメーターは、@ValueSource アノテーションを使って指定します。パラメーターの型に応じて、intsstringsdoubles プロパティなどがあります。

@ParameterizedTest
@ValueSource(ints = {1, 2, 100})
void positiveNumber(int n) {
    assertTrue(isPositiveNumber(n));
}
@ParameterizedTest
@ValueSource(strings = {"Java", "java", "JAVA"})
void upperCase(String s) {
    assertEquals("JAVA", s.toUpperCase());
}

IDE で実行すると、各パラメーターごとのテスト結果が分かりやすくフィードバックされます。IntelliJ IDEA だと特定のパラメーターだけテストを再実行することもできます。

暗黙的な型変換

@ValueSource で指定できるパラメーターはプリミティブ系に限定されていますが、その他のよく使う型のために、String からの暗黙的な型変換がサポートされています。日付に関するテストを書きたい場合には、これがめちゃくちゃ便利です。

@ParameterizedTest
@ValueSource(strings = {"2016-01-01", "2020-01-01", "2020-12-31"})
void leapYear(LocalDate date) {
    assertTrue(date.isLeapYear());
}

サポートされる型変換の詳細は、公式ドキュメント を参照してください。

MethodSource

@ValueSource では1度に1つのパラメーターしか与えることができませんが、@MethodSource を使うことで以下が可能になります。

  • 2つ以上のパラメーターを与える
  • 独自の型のパラメーターを与える
  • 動的に生成したパラメーターを与える

パラメーターとして期待値も一緒に与えることで、Go 言語で推奨されているような Table Driven Test が簡単に書けます。String や boolean を返すだけのユーティリティメソッドのテストを書きたい場合には、これが特に重宝します。

@ParameterizedTest
@MethodSource("japaneseDateProvider")
void japaneseEra(JapaneseDate date, String expected) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("G").withLocale(Locale.JAPAN);
    assertEquals(expected, formatter.format(date));
}

static Stream<Arguments> japaneseDateProvider() {
    return Stream.of(
        arguments(JapaneseDate.of(1989, 1, 7), "昭和"),
        arguments(JapaneseDate.of(1989, 1, 8), "平成"),
        arguments(JapaneseDate.of(2019, 4, 30), "平成"),
        arguments(JapaneseDate.of(2019, 5, 1), "令和")
    );
}

CsvSource

@MethodSource は、いちいち別メソッドを定義しなければならないのと、テストデータ以外のノイズが若干多いのが玉に瑕です。独自の型が必要ない場合は、@CsvSource を使うとこの問題に対処することができます。

カンマで区切られたフィールドがそれぞれパラメーターとなります。先ほど説明した 暗黙的な型変換 の仕組みにより、String から自動的にメソッド引数の型にマッピングされます。

@ParameterizedTest
@CsvSource({
    "1989-01-07, 昭和",
    "1989-01-08, 平成",
    "2019-04-30, 平成",
    "2019-05-01, 令和"
})
void japaneseEra(LocalDate date, String expected) {
    JapaneseDate d = JapaneseDate.from(date);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("G").withLocale(Locale.JAPAN);
    assertEquals(expected, formatter.format(d));
}

enum も暗黙的な型変換の対象なので、次のようなコードも書けてしまいます。手軽で非常に便利なのですが、enum 定数名の名前をリファクタリングしたときに影響範囲を修正し忘れてしまいそうだな、とは思っています。そのシーンにまだなったことがないので、なんとも言えませんが。

@ParameterizedTest
@CsvSource({
    "JANUARY, false, 31",
    "FEBRUARY, false, 28",
    "FEBRUARY, true, 29"
})
void daysOfMonth(Month month, boolean leapYear, int expected) {
    assertEquals(expected, month.length(leapYear));
}

なお、空文字列を含めたい場合は "2, '', true" のようにシングルクォートで囲みます。逆に、シングルクォートなしで "2, , true" とすると null になるので注意が必要です。

まとめ

JUnit 5 で刷新されたパラメーター化テストについて、簡単に紹介しました。JUnit 4 のパラメーター化テストはどうも分かりづらくて、自分でループを書いて回避するシーンが多々ありましたが、JUnit 5 では非常に直感的に使えるのでこれはオススメです。

参考文献