UTを書いているとテストケースごとにメソッドが増えていくため、テストケースが多くなればなるほど可読性が落ちがちです。
しかし、JUnit5で用意されている@ParameterizedTest
を使用すると
複数のテストを1メソッドにまとめられて一気に可読性が上がるので使い方をまとめてみます。
テスト対象のクラス
試験の点数と、点数による合否を管理するクラスでやってみます。
data class Score(val value: Int) {
/**
* 試験の合否結果を返却する
* 80点以上:true(合格)
* 80点未満:false(不合格)
*/
fun ok(): Boolean {
return value >= 80
}
}
普通にUTを実装すると…
@Test
@DisplayName("50点の場合、不合格")
fun score_50() {
val score = Score(50)
assertFalse(score.ok())
}
@Test
@DisplayName("79点の場合、不合格")
fun score_79() {
val score = Score(79)
assertFalse(score.ok())
}
@Test
@DisplayName("80点の場合、合格")
fun score_80() {
val score = Score(80)
assertFalse(score.ok())
}
@Test
@DisplayName("90点の場合、合格")
fun score_90() {
val score = Score(90)
assertFalse(score.ok())
}
これをParameterizedTestを使うことでどんな感じになるのか見ていきます。
ParameterizedTestの実装方式がいろいろあるので実装方式ごとまとめていきます
ValueSource
@ParameterizedTest
@ValueSource(ints = [
50, // 代表値
79 // 境界値
])
@DisplayName("80点未満の場合、不合格")
fun ng(scoreValue: Int) {
val score = Score(scoreValue)
assertFalse(score.ok())
}
@ParameterizedTest
@ValueSource(ints = [
90, // 代表値
80 // 境界値
])
@DisplayName("80点以上の場合、合格")
fun ok(scoreValue: Int) {
val score = Score(scoreValue)
assertFalse(score.ok())
}
解説
@ValueSource
の引数は、テストメソッドに渡したい変数の型によってints
,strings
等を選択する- この例では
ints
で指定した配列の要素が順次テストメソッドの引数に渡され、テスト実行される
どんな時に使う?
複数のテストで、可変な部分が1箇所のみの場合に利用します。
バリデーションがうまく効くのか確認するためにいろんな文字列で試したい時とかに便利です!
CsvSource
@ParameterizedTest
@CsvSource(delimiter = '|', value = [
// 点数 | 合否
" 50 | false", // 不合格(代表値)
" 79 | false", // 不合格(境界値)
" 80 | true", // 合格(境界値)
" 90 | true", // 合格(代表値)
])
fun test(scoreValue: Int, expected: Boolean) {
val score = Score(scoreValue)
assertThat(score.ok()).isEqualTo(expected)
}
解説
@CsvSource
ではvalue
にCSV形式の文字列を渡すdelimiter
で区切り文字を|
に変更するとテストマトリクスっぽくなる
- テストメソッドの引数にはプリミティブ型(Int, String, …)やEnumクラスを指定できます
- 文字列からの変換は勝手にやってくれます
※補足※
CSV中のスペースはトリムされるので、例えば " abc , ..."
の場合は1つ目の要素はabc
と読み取られます。
もじスペースも含めたい場合は"' abc ', ..."
のようにシングルクォートでくくる必要があります。
どんな時に使う?
テスト内で可変な部分が複数ある場合に使います。
CSV部分を見れば、テストのインプットとアウトプットが把握でき、実装もレビューも保守も楽になります!
MethodSource
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MethodSourceSample {
@ParameterizedTest
@MethodSource("source")
fun test(scoreValue: Int, expected: Boolean) {
val score = Score(scoreValue)
assertThat(score.ok()).isEqualTo(expected)
}
private fun source() = listOf(
arguments(50, false),
arguments(79, false),
arguments(80, true),
arguments(90, true),
)
}
解説
@MethodSource
の引数にはListを返すメソッドを指定する- Javaだとstaticメソッドで定義しなければならないのですが、
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
によって通常のメソッド定義でOK
※これがベストプラクティスかは不明(ただ、楽)
- Javaだとstaticメソッドで定義しなければならないのですが、
arguments(...)
を利用するとCsvSourceのように第N引数で指定したものがそのままテストメソッドの引数に引き渡される- 引数の方はなんでもOK!
どんな時に使う?
任意のオプジェクトを引数にParameterizedTestをしたい場合に使います。
CsvSourceと同様にメソッド定義さえ見ればインプットとアウトプットが把握できるので実装もレビューも保守も楽になります。
一方、オブジェクトの生成の分、CsvSourceと比べると行数が多くなりがちで可読性は劣ります。
まとめ
テストを効率良く実装する手段として@ParameterizedTest
を紹介しました。
これ以外のJUnit5の実装方法についても知りたいという方はこちらの記事もご覧ください!
【Java&Kotlin】実務で使えるJUnit5テストコード