ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション(LDI)の技術ブログです

【Android】パラメタライズドテストの導入

はじめに

Androidエンジニアの篠本(ささもと)です。

今回は、テストコードのメンテナンス性向上を目的にパラメタライズドテストを導入しましたので、パラメタライズドテストについて紹介をさせていただきます。

なお、この記事は以下の環境を前提としています。

  • Kotlin 1.9.24
  • Android APIレベル 35
  • JUnit 4.13.2

パラメタライズドテストとは?

パラメタライズドテストとは、テストの値をパラメータ化し、複数のテストケースを1つにまとめることで、同じテストロジックを異なる入力値や条件で繰り返し実行するテスト手法です。

これにより、重複したテストコードを書く必要がなくなるため、テストコードが読みやすく保守もしやすくなります。また、テストコードからテストケースの網羅性が視認しやすくなるため、テストケースの漏れに気づきやすく、ソースコードの品質向上が期待できます。

たとえば、以下のような同じ数同士を足し算する関数のテストコードを書くことを考えます。

fun add(value: Int): Int {
    return value + value
}

この関数に対して、1同士を足す場合と2同士を足す場合のテストコードを書くとき、通常では以下のようなテストコードを書きます。

@Test
fun 同じ数同士の足し算_1同士を足す_結果が2であること() {
    // 実行
    val actual = add(1)

    // 検証
    assertThat(actual).isEqualTo(2)
}

@Test
fun 同じ数同士の足し算_2同士を足す_結果が4であること() {
    // 実行
    val actual = add(2)

    // 検証
    assertThat(actual).isEqualTo(4)
}

上記のテストコードでは、テスト対象の関数呼び出しと関数の結果を検証するテストロジックは共通であり、テスト対象の関数に渡す引数と期待する結果が異なるのみです。

上記の場合ではテストメソッドが2つしかないため気になりませんが、テストメソッドの数が多くなるとテストクラスの行数が長くなります。

その場合、たとえばテストが漏れなく網羅できているか管理しづらくなるなど、メンテナンス性が低くなってしまいます。

パラメタライズドテストを導入すると、以下のようにテストのロジックとパラメータを分けてテストコードを記述することができます。

// テストパラメータを格納する入れ物
data class TestCase(
    val input: Int,
    val expected: Int
)

@Test
fun 同じ数同士の足し算_パラメータのリストを渡す_結果が同じ数同士の和であること() {
    // case: TestCase

    // 実行
    val actual = add(case.input)

    // 検証
    assertThat(actual).isEqualTo(case.expected)
}

実際にテストコードを書く際には、使用するテストフレームワークにパラメタライズドテストを書く仕組みが備わっているため、それを利用してパラメタライズドテストを書きます。

ローソンアプリではJUnit4を使用してテストコードを書いているため、JUnit4にてパラメタライズドテストを書く方法を具体的に説明します。

パラメタライズドテストの書き方

JUnit4にてパラメタライズドテストを書く場合、以下のように書きます。

@RunWith(Parameterized::class)
class AddParameterizedTest(
    private val case: TestCase
) {

    data class TestCase(
        val input: Int,
        val expected: Int
    )

    companion object {
        @JvmStatic
        @Parameterized.Parameters
        fun case() = listOf(
            TestCase(input = 1, expected = 2),
            TestCase(input = 2, expected = 4)
        )
    }

    @Test
    fun 同じ数同士の足し算_パラメータのリストを渡す_結果が同じ数同士の和であること() {
        // 実行
        val actual = add(case.input)

        // 検証
        assertThat(actual).isEqualTo(case.expected)
    }
}

まずテストクラスの宣言では、@RunWith(Parameterized::class)を付与してそのテストクラスがパラメタライズドテストであることをJUnit4に知らせます。

そして、テストパラメータが格納されているTestCaseをコンストラクタ引数に追加し、後述するテストメソッドがテストパラメータを受け取れるようにします。

@RunWith(Parameterized::class)
class AddParameterizedTest(
    private val case: TestCase
) {
    // 省略
}

次にテストパラメータを格納するTestCaseの宣言と、TestCaseに格納するテストパラメータを定義します。

テストパラメータはcompanion objectに定義し、@JvmStatic@Parameterized.Parametersを付与します。

JUnit4でテストパラメータを定義する場合、シングルトンにて定義する必要があるため、テストパラメータはcompanion objectに定義し、@JvmStaticを付与する必要があります。

また、テストパラメータに@Parameterized.Parametersを付与することで、JUnit4にそれがテストパラメータであることを知らせます。

@RunWith(Parameterized::class)
class AddParameterizedTest(
    private val case: TestCase
) {

    data class TestCase(
        val input: Int,
        val expected: Int
    )

    companion object {
        @JvmStatic
        @Parameterized.Parameters
        fun case() = listOf(
            TestCase(input = 1, expected = 2),
            TestCase(input = 2, expected = 4)
        )
    }

    // 省略
}

最後にテストメソッドを書きます。

前述の通り、テストパラメータのcaseをコンストラクタ引数に追加しているため、テストパラメータをcaseから取り出すことができます。

@RunWith(Parameterized::class)
class AddParameterizedTest(
    private val case: TestCase
) {

    // 省略

    @Test
    fun 同じ数同士の足し算_パラメータのリストを渡す_結果が同じ数同士の和であること() {
        // 実行
        val actual = add(case.input)

        // 検証
        assertThat(actual).isEqualTo(case.expected)
    }
}

最後に

パラメタライズドテストを導入することにより、テストロジックが共通でテストパラメータだけが異なる複数のテストメソッドを共通化できるようになりました。パラメタライズドテストを活用できるところでは積極的に活用し、メンテナンス性が高いテストコードを書いていきたいと考えています。

今後もローソンデジタルイノベーションでは技術ブログを更新していきますので、是非「読者になる」で応援していただけますと幸いです。