【Kotlin】ParameterizedTestでJUnitの可読性向上

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の引数は、テストメソッドに渡したい変数の型によってintsstrings等を選択する
  • この例ではintsで指定した配列の要素が順次テストメソッドの引数に渡され、テスト実行される

どんな時に使う?

複数のテストで、可変な部分が1箇所のみの場合に利用します。
バリデーションがうまく効くのか確認するためにいろんな文字列で試したい時とかに便利です!

メモ
今回のテスト対象だと、点数と期待値が可変と認識して、CsvSourceを使う方がオススメ!

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部分を見れば、テストのインプットとアウトプットが把握でき、実装もレビューも保守も楽になります!

メモ
とりあえず@CsvSourceでのParameterizedTestが実装できないか検討した方がいいくらいにオススメです!

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
      ※これがベストプラクティスかは不明(ただ、楽)
  • arguments(...)を利用するとCsvSourceのように第N引数で指定したものがそのままテストメソッドの引数に引き渡される
  • 引数の方はなんでもOK!

どんな時に使う?

任意のオプジェクトを引数にParameterizedTestをしたい場合に使います。
CsvSourceと同様にメソッド定義さえ見ればインプットとアウトプットが把握できるので実装もレビューも保守も楽になります。
一方、オブジェクトの生成の分、CsvSourceと比べると行数が多くなりがちで可読性は劣ります。

メモ
オブジェクト生成をテストメソッドの中に押し込めるようであればCsvSourceで実装するのがオススメです!

まとめ

テストを効率良く実装する手段として@ParameterizedTestを紹介しました。

これ以外のJUnit5の実装方法についても知りたいという方はこちらの記事もご覧ください!

【Java&Kotlin】実務で使えるJUnit5テストコード

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です