Kotlin における ParametrizedTest の MethodSource の利用を便利にする

JUnit5 で 2.17. Parametrized Tests が利用できるようになった。
これによって簡単にテーブル駆動のような input のバリエーションに着目するようなテストをより簡単に書くことができるようになった。

@ParameterizedTest
@ValueSource(strings = ["racecar", "radar", "able was I ere I saw elba"])
fun palindromes(candidate: String) {
    assertTrue(StringUtils.isPalindrome(candidate))
}

※ 上記は 2.17. Parametrized Tests にある例を Kotlin 化したもの

上記のように @ParameterizedTest@ValueSource のようなデータソースの定義をセットで書くというのが基本の形となっている。
@ValueSource の他にも以下のようなデータソースの定義がある。

※ 詳細は、 2.17.3. Sources of Arguments を参照すること。

シンプルなケースの場合には、@ValueSource@EnumSource で済ませる方が冒頭のようにシンプルなので嬉しいと考えている。

ただし割と機能としては貧弱でちょっとでも複雑なことをしようと思うと、 @MethodSource@FieldSource を利用したくなるものの Kotlin では以下の記述のように static に定義する必要があり不便さがある。


Factory methods within the test class must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS); whereas, factory methods in external classes must always be static.

Fields within the test class must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS); whereas, fields in external classes must always be static.

つまり Kotlin だと static がないので以下のように companion object の中に書く必要がある。

class SumTest {
    companion object {
        @JvmStatic
        fun sumProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.of(1, 2, 3),
                Arguments.of(5, 10, 15)
            )
        }

        // ...
        // other data sources
        // ...
    }

    // ...
    // other tests
    // ...

    @ParameterizedTest
    @MethodSource("sumProvider")
    fun sum(a: Int, b: Int, expected: Int) {
        Truth.assertThat((a + b)).isEqualTo(expected)
    }
}

テストクラスが小さい場合にはこれでも良いものの、ケースが増えたりデータソースが増えてくると「データソースとして定義されているメソッド」と「テストにおける利用箇所」の距離がどんどん離れていってしまい可読性が落ちるという課題がある。

なんとかして「データソースとして定義されているメソッド」と「テストにおける利用箇所」の距離を短くして可読性を上げたいというので調べてみると

https://discuss.kotlinlang.org/t/junit5-parameterized-tests/28168

という discussion を投稿している人がいてそれを参考に以下のような Helper を作ることで落ち着いた

open class ParametrizedTestArgumentsProviderHelper(private vararg val arguments: Arguments) : ArgumentsProvider {
    override fun provideArguments(
        context: ExtensionContext?
    ): Stream<out Arguments> = Stream.of(*arguments)
}

使い方は以下のようになる

class SumTest {
    // ...
    // other data sources
    // ...

    // ...
    // other tests
    // ...

    class SumProvider : ParametrizedTestArgumentsProviderHelper(
        Arguments.of(1, 2, 3),
        Arguments.of(5, 10, 15)
    )

    @ParameterizedTest
    @ArgumentsSource(SumProvider::class)
    fun sum(a: Int, b: Int, expected: Int) {
        Truth.assertThat((a + b)).isEqualTo(expected)
    }

    // ...
    // other data sources
    // ...    

    // ...
    // other tests
    // ...

}

都度内部クラスを定義するというところは不便ではあるが、これによって「データソースとして定義されているメソッド」と「テストにおける利用箇所」の距離を短くして可読性を上げることができていると考えている。