概要
ロジックや仕様をテストで守っていると開発がしやすいですよね。
リファクタが気軽にできるようになったり、デグレをテストが教えてくれたり、画面試験の前にエラーに気づけたり。
そんなテストを実践で使っているなか、可読性を大きく改善した取り組みがあったので紹介します。
結論を言うと Parameterized Test を使うのですが、最初から入っているものは可読性/メンテナビリティにストレスがあったので使いません!
とてもシンプルな関数を足すことで Parameterized Test 的なことを可読性/メンテナビリティ高く実現できましたので紹介します。
Parameterized Test
テストの可読性をあげるなら Parameterized Test の思想は欠かせません。
Parameterized Test というのは複数のケースを1つにまとめて記載する方法で、差分だけを引数で渡す形でテストできるものです。
重複コードが減りますし、確認するコードが引数によってどういう変化を起こすのか直感的にわかるようにできます。
JUnit5 にはこれを行うための @ParameterizedTest
という仕組みがありますが(JUnit4 でもモジュールを入れることで同等のことができるのですが)、ここではさらに見やすくするために行ったことをご紹介します。
@ParameterizedTest
の何が不満だったのか
たとえばこういうイメージ:
@ParameterizedTest
@ArgumentsSource(TestCase::class)
fun hoge(value: Int, expected: Int) {
val actual = hoge.aaa(value)
Assert.assertEquals(expected, actual)
}
private class TestCase : ArgumentsProvider {
override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> = Stream.of(
arguments(1, 101),
arguments(2, 102),
)
}
テストの記述とテストケースの記述が離れてしまうんですよね。
これがテストケースがたくさん並んでくると結構ストレスでした。
改善版
これを解決するために下記のクラスを作成しました。
abstract class TestBase {
open setUp() {
}
fun <T> cases(vararg cases: T, block: (T) -> Unit) {
for (c in cases) {
setUp()
try {
block(c)
} catch (e: Throwable) {
System.err.println("CASE = $c")
throw e
}
clearAllMocks()
}
}
}
このシンプルなクラスを作るだけで Parameterized Test が可読性高く書けるようになります。
// TestBase を継承して利用します。
@Test
fun hoge() = cases(
1 to 101,
2 to 102,
) { (value, expected) ->
val actual = hoge.aaa(value)
Assert.assertEquals(expected, actual)
}
エラーの場合は、どのケースでエラーが出たのかは冒頭で出力されるので特定できます。
簡単ですね!
たくさんのデータを引数で渡したいなら
data class 作ればOKです。
@Test
fun hoge() = cases(
HogeData(1, 2, 3, 101),
HogeData(4, 5, 6, 102),
) { c ->
val actual = hoge.aaa(c.x, c.y, c.z)
Assert.assertEquals(c.expected, actual)
}
data class HogeData(
val x: Int,
val y: Int,
val z: Int,
val expected: Int,
)
簡単ですよね!