- はじめに
- JUnit5 の導入
- CsvSource による Parameterized Test
- Parameterized Test の型変換
- ArgumentsAccessor で引数をまとめて受け取る
- ArgumentConverter で任意のオブジェクトを受け取る
- ArgumentsAggregator で複雑なインスタンスを扱う
- MethodSource でプログラマティックにパラメータを生成する
- その他の話題
- まとめ
はじめに
2017年9月11日にリリースされた JUnit 5 ですが、早いものでもう2年が経過しました。
現在も JUnit4 系を利用している現場も多いと思いますが、JUnit5 の @ParameterizedTest
を使うためにも早い目に移行することをおすすめします。
JUnit4 系と JUnit5 系は共存することもできますし、JUnit5 のその他の機能は使わずとも、Parameterized Test のためだけにでも導入する価値があると思います。
本記事では、Parameterized Test について、実際のテストで良く使う機能を中心に説明していきます。
JUnit5 の導入
Gradle(5系を想定します) を使う場合、build.gradle
は以下のように定義します。
plugins { id 'java' } repositories { jcenter() } dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.5.2' testCompile 'org.assertj:assertj-core:3.14.0' } test { useJUnitPlatform() }
testImplementation
で junit-jupiter-api
を指定し、testRuntimeOnly
で junit-jupiter-engine
を指定します。
今回説明する @ParameterizedTest
は、上記に加え junit-jupiter-params
が必要になりますので、合わせて指定します。
なお、JUnit5 からは Matcher を選択的に導入するようになっています。ここでは、現時点で最も使われているであろう Assertj を導入しました。
Gradle Java 以外の導入については以下のリポジトリのサンプルを参照してください。
CsvSource による Parameterized Test
Parameterized Test は、テストメソッドへのパラメータを様々な方法で提供することで、異なるパラメータのテストを簡素に記述することができます。
最初に、最もよく使うであろう、パラメータをCSV形式で定義できる @CsvSource
から見ていきます。
@CsvSource
を使ったテストメソッドは以下のようになります。
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; ... import static org.assertj.core.api.Assertions.*; class AppTest { @ParameterizedTest @CsvSource({ "2019-01-01, 1, 2019-01-02", "2019-01-01, 30, 2019-01-31", "2019-01-01, 40, 2019-02-10", }) void testWithCsvSource(String baseDateText, int amountToAdd, String expectedText) { LocalDate actual = LocalDate.parse(baseDateText).plusDays(amountToAdd); LocalDate expected = LocalDate.parse(expectedText); assertThat(actual).isEqualTo(expected); } }
@ParameterizedTest
でアノテートし、パラメータを @CsvSource
で定義します。
このテストでは、日付の加算をテストしています。
@CsvSource
で指定したパラメータを引数にテストメソッドが実行され、以下のような結果が得られます。
パラメータに ,
を含む場合は、' '
で括ることで1つの項目として扱われます。
foo
と baz, qux
の 2つをパラメータとして渡したい場合は @CsvSource({ "foo, 'baz, qux'" })
のように指定します。
またダブルクオート "
をパラメータ値に含ませたい場合は \"
のようにエスケープすることができます。
CSVのデリミタである ,
は、以下のようにして変更することができます。
@CsvSource(value = { "2019-01-01 | 1 | 2019-01-02", "2019-01-01 | 30 | 2019-01-31", "2019-01-01 | 40 | 2019-02-10", }, delimiter = '|')
パラメータの数が多い場合は @CsvFileSource
を使うことで外部CSVからパラメータ生成を行うこともできます。
@CsvFileSource(resources = "/arguments.csv", numLinesToSkip = 1)
大量のパラメータはCSVエディタなどで編集できるとはかどります。
Parameterized Test の型変換
上記例では String と int を引数としていましたが、Parameterized Test では暗黙的な型変換が自動的に行われるため、以下のように書くこともできます。
@ParameterizedTest @CsvSource(value = { "2019-01-01, 1, 2019-01-02", "2019-01-01, 30, 2019-01-31", "2019-01-01, 40, 2019-02-10", }) void testWithCsvSource(LocalDate base, int amountToAdd, LocalDate expected) { LocalDate actual = base.plusDays(amountToAdd); assertThat(actual).isEqualTo(expected); }
引数の型を LocalDate に変更しました。これにより @CsvSource
で指定した文字列のパラメータが型変換され LocalDate として得ることができます。
Integer や Boolean だったり、File、BigDecimal、URI、URI、java.time 系については文字列からオブジェクト型へ変換が自動的に行われます。
さらに、引数として指定するクラスに以下のルールを満たすメソッドがあれば、文字列からオブジェクトへの変換が自動的に行われます。
ファクトリメソッド: ターゲット型に宣言された非 private の static メソッドで、 String 型の引数を1つだけ受け取り、ターゲット型のインスタンスを返すメソッド(メソッド名は任意)
ファクトリコンストラクタ: ターゲット型に宣言された非 private のコンストラクタで、 String 型の引数を1つだけ受け取ってインスタンスを作るコンストラクタ
双方が合致する場合は ファクトリメソッド が優先されます。
ArgumentsAccessor で引数をまとめて受け取る
引数の数が多く見通しが悪い場合は、ArgumentsAccessor で引数をまとめて受け取ることができます。
@ParameterizedTest @CsvSource(value = { "2019-01-01, 1, 2019-01-02", "2019-01-01, 30, 2019-01-31", "2019-01-01, 40, 2019-02-10", }) void testWithCsvSource(ArgumentsAccessor arguments) { LocalDate baseDate = arguments.get(0, LocalDate.class); int amountToAdd = arguments.getInteger(1); LocalDate expected = arguments.get(2, LocalDate.class); LocalDate actual = baseDate.plusDays(amountToAdd); assertThat(actual).isEqualTo(expected); }
ArgumentsAccessor
には getInteger()
などのプリミティブ型を取得するメソッドが提供されており、引数のインデックスを指定して引数を取得できます。
その他のオブジェクト型の場合は引数でターゲットタイプを指定することでキャスト無しで値を取得できます。
ArgumentConverter で任意のオブジェクトを受け取る
暗黙的な型変換が行えないクラスを扱いたい場合は、コンバータを定義することで恣意的な型変換を行うことができます。
以下のようなコンバータを定義します。
public static class YmdArgumentConverter extends SimpleArgumentConverter { DateTimeFormatter ymd = DateTimeFormatter.ofPattern("yyyy/MM/dd"); @Override public Object convert(Object input, Class<?> targetClass) throws ArgumentConversionException { if (!LocalDate.class.isAssignableFrom(targetClass)) { throw new ArgumentConversionException( "Cannot convert to " + targetClass.getName() + ": " + input); } return LocalDate.parse(input.toString(), ymd); } }
日付文字列を "yyyy/MM/dd" 形式で変換できるコンバータを作成しました。
@ConvertWith
アノテーションで引数にコンバータを指定します。
@ParameterizedTest @CsvSource(value = { "2019/01/01, 1, 2019/01/02", "2019/01/01, 30, 2019/01/31", "2019/01/01, 40, 2019/02/10", }) void testWithCsvSource( @ConvertWith(YmdArgumentConverter.class) LocalDate base, int amountToAdd, @ConvertWith(YmdArgumentConverter.class) LocalDate expected) { LocalDate actual = base.plusDays(amountToAdd); assertThat(actual).isEqualTo(expected); }
これにより、任意のクラスの型変換を行うことができます。
プロジェクトで用意した Value Object などはコンバータを用意しておくと便利でしょう。
ArgumentsAggregator で複雑なインスタンスを扱う
ここまでのテストケースは引数の数も少なく単純なものでした。
@AggregateWith
を使うことで、実プロジェクトで扱うような大きなインスタンスを引数で受けることができます。
ArgumentsAggregator
の実装クラスを以下のように定義します。
public class PersonAggregator implements ArgumentsAggregator { @Override public Person aggregateArguments( ArgumentsAccessor arguments, ParameterContext context) { return new Person(arguments.getString(0), arguments.getString(1), arguments.get(2, Gender.class), arguments.get(3, LocalDate.class)); } }
ここでは Person
オブジェクトを生成するものとして定義しています。
テストメソッド側では @AggregateWith
で Aggregator を指定します。
@ParameterizedTest @CsvSource({ "Jane, Doe, F, 1990-05-20", "John, Doe, M, 1990-10-22" }) void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) { ... }
これにより様々な特性を持った Person オブジェクトに対するテストが行えるようになります。
Aggregator を複数用意すれば、以下のように複数のオブジェクトを取得することもできます。
@ParameterizedTest @CsvSource({ "Jane, Doe, F, 1990-05-20, book1, 1200", "John, Doe, M, 1990-10-22, book2, 980" }) void testWithArgumentsAggregator( @AggregateWith(PersonAggregator.class) Person person, @AggregateWith(BookAggregator.class) Book book) {
MethodSource でプログラマティックにパラメータを生成する
今まではアノテーション内に定義した CSV でパラメータを指定していましたが、@MethodSource
にてパラメータをプログラマティックに生成することができます。
@ParameterizedTest @MethodSource("methodSourceProvider") void testWithMethodSource(String actualText, int amountToAdd, String expectedText) { ... } static Stream<Arguments> methodSourceProvider() { return Stream.of( arguments("2019/01/01", 1, "2019/01/02"), arguments("2019/01/01", 30, "2019/01/31"), arguments("2019/01/01", 40, "2019/02/10")); }
Stream<Arguments>
を返す static メソッドを用意し、@MethodSource("methodSourceProvider")
として指定することで、メソッドで生成した値を引数に与えることがでます。
@MethodSource
の引数を省略した場合、テストメソッドと同じ名前をもつファクトリメソッドを自動的に探します。
つまり以下のようになります。
@ParameterizedTest @MethodSource void testWithMethodSource(String actualText, int amountToAdd, String expectedText) { ... } static Stream<Arguments> testWithMethodSource() { ... }
引数のプロバイダは別クラスに定義することもできます。
package example; class PersonProviders { static Stream<Arguments> personProvider() { return Stream.of( new Person("Jane", "Doe", F, LocalDate.parse("1990-05-20")), new Person("John", "Doe", M, LocalDate.parse("1990-10-22"))); } }
先ほどの例では、arguments としてパラメータ生成をしていましたが、このように直接オブジェクトを生成することもできます。
@MethodSource
では以下のように完全修飾した形でプロバイダを指定します。
@ParameterizedTest @MethodSource("example.PersonProviders#personProvider") void testWithMethodSource(Person person) { ... }
その他の話題
ParameterizedTest の表示
Parameterized Test は以下のようにすることで、テスト結果の表示をカスタマイズすることができます。
@DisplayName("LocalDate plusDays") @ParameterizedTest(name = "{index} ==> base=''{0}'', amountToAdd={1}, expected=''{2}''") @CsvSource(value = { "2019-01-01 | 1 | 2019-01-02", "2019-01-01 | 30 | 2019-01-31", "2019-01-01 | 40 | 2019-02-10", }, delimiter = '|') void testWithCsvSource(ArgumentsAccessor arguments) { ... }
'{0}'
とすると {0}
という文字列が出力されるため、'
を出力するには ''{0}''
のように指定します。
以下のようなテスト結果が得られます。
単項目
実際のテストではあまり使いませんが、@ValueSource
にて単項目のパラメータ生成を行うことができます。
@ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) void testWithValueSource(int argument) { assertThat(argument).isBetween(1, 3); }
プリミティブ型や文字列の生成が行えます。
まとめ
JUnit5 における Parameterized Test の使い方について説明しました。
各種アノテーションによる様々なパラメータ生成方法があることが分かりました。
@CsvSource
: CSV形式の文字列からパラメータを生成@CsvFileSource
: CSVファイルからパラメータを生成@ConvertWith
: ArgumentConverter でパラメータの型変換を定義@AggregateWith
: Aggregator でパラメータを集約してオブジェクトを生成@MethodSource
: メソッドでパラメータを生成
旧来ではパラメータ値を変化させたテストが書きにくかったですが、JUnit5 の Parameterized Test 機能を使うことで、簡単に可読性の高いテストを書くことができます。
- 作者: Kent Beck,和田卓人
- 出版社/メーカー: オーム社
- 発売日: 2017/10/14
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
- 作者: Jeff Langr,Andy Hunt,Dave Thomas,牧野聡
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/09/02
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (9件) を見る
JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)
- 作者: 渡辺修司
- 出版社/メーカー: 技術評論社
- 発売日: 2012/11/21
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 273回
- この商品を含むブログ (69件) を見る