概要
1つのテストに複数のデータを適用したいときにはパラメータ化テストが有効です。
ここでは様々なパラメータ化テストを利用シーンに合わせて解説していきます。
パラメータ化テストを実装する時は、@ParameterizedTestをテストメソッドに付与します。
準備
パラメータ化テストを実装するには、junit-jupiter-paramsの依存関係を追加する必要があります。
そのため、build.gradleに以下を追加します。
https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params
単一データ
テストデータとして単一なものを扱います。
@ValueSource
StringやIntegerといったリテラル値を配列で与えることができます。
String型のテスト
@ValueSourceにstringsを引数名して、値は配列で与えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.ParameterizedTest; import static org.junit.jupiter.api.Assertions.*; public class ParameterizedTestCases { /** * テスト用のInnerクラス */ class Item { private String name; public Item(String name) { if (name.length() > 10) { throw new IllegalArgumentException(); } this.name = name; } } /** * Stringのケース(正常系) * @param name */ @ParameterizedTest @ValueSource(strings = { "", "a", "1234567890" }) void testItemNameCanSetting(String name){ assertDoesNotThrow(() -> new Item(name)); } /** * Stringのケース(異常系) * @param name */ @ParameterizedTest @ValueSource(strings = { "12345678901" }) void testItemNameCanNotSetting(String name){ System.out.println(name); assertThrows(IllegalArgumentException.class ,() -> new Item(name)); } } |
実行結果
テストデータの結果が成功か失敗かを各テスト毎に確認することができます。
※成功した結果をIntelliJで確認する場合、画像左上の☑マークをONにすること。
他の型について
String以外のオプションは下記ドキュメントの「Optional Element Summary」に一覧化されています。
Annotation Type ValueSource
Class型のテスト
StringやIntegerは簡単ですが、Class型はどう扱うかのか私はピンときませんでした。
なので少し調べたところ、簡単な例があったので紹介します。
Class型のテストはあまり使う機会はないと思いますが、こういった使い方なのだということがわかっておけばいいかなと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.ParameterizedTest; import static org.junit.jupiter.api.Assertions.*; public class ParameterizedTestCases { /** * Classの使用例 * @param argument */ @ParameterizedTest @ValueSource(classes = { String.class, Integer.class }) void testContainJavaLangPkg(Class<?> argument) { assertEquals( "java.lang", argument.getPackage().getName()); } } |
参考
What is the use of @ValueSource(classes= …..)
複数データ
複数のデータをテストデータとして扱います。
また、引数に与えるデータは暗黙的に型変換されます。(2.15.4. Argument Conversion)
@CsvSource
テストデータは基本的にCSVで与えますが、カンマ以外を区切り文字にするオプションなども用意されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
import org.junit.jupiter.params.aggregator.ArgumentsAccessor; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.ParameterizedTest; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.*; public class ParameterizedTestCases { static class Calculator { public static int add(int added, int add) { return added + add; } } /** * 複数のテストデータを引数(CSV)として扱います。 */ @ParameterizedTest @CsvSource({ "1, 1, 2", "1, -1, 0", "-1, 1, 0", }) void testAdd(int added, int add, int expected) { assertThat(Calculator.add(added, add), is(expected)); } /** * もし引数が多くなる場合、単一の引数にまとめることもできます。 * 値は暗黙的な型変換が行われるため、適切なGetterとIndexを指定する必要があります。 */ @ParameterizedTest @CsvSource({ "1, 1, 2", "1, -1, 0", "-1, 1, 0", }) void testAddSingleArg(ArgumentsAccessor arg) { assertThat(Calculator.add(arg.getInteger(0), arg.getInteger(1)), is(arg.getInteger(2))); } /** * 区切り文字がタブの場合、オプションで指定することができます。 * 複数オプションを使用する場合、[value]は明示的に宣言する必要がある。 */ @ParameterizedTest @CsvSource( delimiterString = "\t", value = { "1\t1\t2", "1\t-1\t0", "-1\t1\t0", }) void testAddDelimiterAsTab(int added, int add, int expected) { assertThat(Calculator.add(added, add), is(expected)); } } |
※テストデータのみであればオプション名[value]は省略できます。
しかし、他のオプション(delimiterString等)を使用した場合、[value]は明示的に指定する必要があります。
@CsvFileSource
CSVファイルをテストデータとして使うことができます。
add.csv
データ
1 2 3 4 |
1,1,2 0,1,1 1,0,1 1,-1,0 |
配置先(src/test/resources/add.csv)
テストコード
テストファイルの配置先を指定するオプションに、resourcesまたはfilesのいずれかを指定できます。
パスの指定方法が異なるので以下を参考にして下さい。
・resources:resourcesディレクトリ配下にある想定でパスを指定
・files :プロジェクト直下にある想定でパスを指定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; public class CsvFileSourceTest { /** * テスト対象の計算クラス */ static class Calculator { public static int add(int added, int add) { return added + add; } } /** * CsvFileSourceをテストデータとする * オプション:resourcesはresourcesディレクトリ直下以降のパスを指定する。 */ @ParameterizedTest @CsvFileSource(resources = "add.csv") void testAddCsvFileSource(int added, int add, int expected) { assertThat(Calculator.add(added, add), is(expected)); } /** * CsvFileSourceをテストデータとする * オプション:filesはsrcディレクトリからのパスを指定する。 */ @ParameterizedTest @CsvFileSource(files = "src/test/resources/add.csv") void testAddCsvFileSourceAsFiles(int added, int add, int expected) { assertThat(Calculator.add(added, add), is(expected)); } } |
@CsvSourceと同様にdelimiterまたはdelimiterStringで区切り文字を指定できるので、TSVのテストも可能です。
Enum
Enum定義をテストするためのテストデータを提供するアノテーションです。
@EnumSource
基本的な使い方は以下のようなパターンです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import java.util.concurrent.TimeUnit; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.params.provider.EnumSource.Mode.MATCH_ANY; public class EnumSourceTest { // 単一のEnumが一致する @ParameterizedTest @EnumSource(value = TimeUnit.class, names = {"SECONDS"}) void timeUnitTest(TimeUnit args){ assertThat(args, is(TimeUnit.SECONDS)); } // 複数のEnumがいずれも含まれる @ParameterizedTest @EnumSource(value = TimeUnit.class, names = {"SECONDS", "MINUTES"}) void timeUnitMultiArgsTest(TimeUnit args){ assertThat(args, isIn(TimeUnit.values())); } // 正規表現に一致するEnumが含まれる @ParameterizedTest @EnumSource(value = TimeUnit.class, mode = MATCH_ANY ,names = {"^.*SECONDS"}) void timeUnitRegexTest(TimeUnit args){ assertThat(args, isOneOf(TimeUnit.NANOSECONDS, TimeUnit.MILLISECONDS, TimeUnit.MICROSECONDS, TimeUnit.SECONDS)); } } |
引数パラメータ
value:Enumのクラスを指定します。
mode:namesで指定する値または正規表現の条件を指定します。
指定方法は以下の4つが可能です。
・MATCH_ANY(部分一致)
・MATCH_ALL(完全一致)
・INCLUDE(含む)
・EXCLUDE(含まない)
names:Enumの値または正規表現を指定します。
ファクトリメソッドによる指定
MethodSourceで注意すべき点は以下の2つです。
・戻り値はStreamとする。
・定義するメソッドはstaticとする。
@MethodSource
単一の値を扱う場合
単純なintやStringの値をテストデータにする場合は、CsvSourceまたはValueSourceで十分です。
もしMethodSourceを使うのであれば、リテラル値でない型であるか、メソッドでテストデータを作るとよい場合です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.IntStream; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; public class MethodSourceTest { /** * ■リテラル値でない型である場合 * ケース:ListがNULL以外の値を持っていること */ @ParameterizedTest @MethodSource("listMethodSource") void listMethodSourceTest(List<String> arg){ assertThat(arg, not(nullValue())); assertThat(arg.size(), greaterThanOrEqualTo(0)); } static Stream<Arguments> listMethodSource() { return Stream.of( arguments(Arrays.asList("a")), arguments(Arrays.asList("x", "y")), arguments(new ArrayList<>()) ); } /** * ■メソッドでテストデータを作る場合 * ケース:偶数かつ100以下の数値であること * 補足 :CsvSourceだと値を51個(0~100)作る必要があるがMethodSourceだと1行で済む。 */ @ParameterizedTest @MethodSource("even") void evenTest(int arg) { assertTrue(arg % 2 == 0); assertThat(arg, lessThanOrEqualTo(100)); } static IntStream even(){ return IntStream.rangeClosed(0, 100).filter(n -> n % 2 == 0); } } |
複数の値を扱う場合
テストの値が複数かつリテラル値以外も扱う場合も有用です。
以下はとても簡単な例ですが、どのように書くかの参考になると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * ■リテラル値とオブジェクト型の混合引数を扱う場合 * ケース:idが1以上かつ、nameがNULLではなく、listの文字列が1文字以上で全て大文字であること */ @ParameterizedTest @MethodSource("multiArgsOfMethodSource") void multiArgsOfMethodSourceTest(int id, String name, List<String> list) { assertThat(id, greaterThanOrEqualTo(1)); assertThat(name, not(nullValue())); assertTrue(list.stream().allMatch(s -> s.matches("^[A-Z]+$"))); } static Stream<Arguments> multiArgsOfMethodSource(){ return Stream.of( arguments(1, "hoge", Arrays.asList("A", "B")), arguments(2, "piyo", Arrays.asList("CC", "DD")), arguments(3, "fuga", Arrays.asList("EEE", "FFF")) ); } |
まとめ
- 1つのテストに複数のデータを適用したいときにはパラメータ化テストを使う。
- パラメータ化テストを実装するには、junit-jupiter-paramsの依存関係を追加する。
- パラメータ化テストとして、@ValueSource、@CsvSource、@EnumSource、@MethodSourceが用意されている。
参考
Junit5 Usere Guide – 2.15. Parameterized Tests
コメント