Spekでパラメーター化テストはどうやって書く?
Androidアプリの開発でSpekを使い始めた際にパラメータ化テストを書きたくなった。その時に考えたことについて書く。
Spekとは
Kotlinのテストフレームワーク。RSpecを参考にしたインタフェースになっている。
Core Concepts - Spek Framework
DroidKaigi 2019 - Spek で UnitTest を書こう
パラメータ化テストとは?
テスト対象の振る舞いに対してデータだけを変えて検証する手段。テストフレームワークがサポートしていることが多い。
これを利用するモチベーションは以下リンク先の説明がわかりやすい。
https://t-wada.hatenablog.jp/entry/design-for-testability#ポイント-パラメータ化テストParameterized-Test
Spekにパラメータ化テストの機能はある?
現状なかった。issueがあったけど進まなさそうな雰囲気。
Spekの1系ではパラメータ化テストの記述のための拡張があったらしい。
(少しissueを見てみたが2系で削除された経緯は分からなかった。)
書いてみる
フレームワークのサポートはなかったので、自分で書いてみる。
Assertion Rouletを避ける
テストコードにおけるアンチパターン。
テストが通っているときは問題ないが、失敗した際には「失敗するとそれ以降のテストケースが実行されない」、「どのケースで失敗したか分かりづらい」という状況に陥ることを指す。
ループを書いてパラメータテストを実現しようとするとハマりがち。
このAssertion Rouletを回避しながら書く。
テスト対象
Epoch Timeを曜日を表すEnumに変換する関数 fromEpoch
を例にする。
素朴な記述(not パラメータ化テスト)
パラメータ化テストの前に、各条件を素朴に記述したテストコードを書いてみる。
やや冗長になっている。
object DayOfWeekTest: Spek({
describe("fromEpoch") {
context("2000-01-02 given") {
val given = getEpoch(year = 2000, month = Calendar.JANUARY, day = 2)
it ("should be sunday") {
Assert.assertSame(
DayOfWeek.SUNDAY,
DayOfWeek.fromEpoch(given)
)
}
}
context("2000-01-03 given") {
val given = getEpoch(year = 2000, month = Calendar.JANUARY, day = 3)
it ("should be sunday") {
Assert.assertSame(
DayOfWeek.SUNDAY,
DayOfWeek.fromEpoch(given)
)
}
}
context("2000-01-04 given") {
val given = getEpoch(year = 2000, month = Calendar.JANUARY, day = 4)
it ("should be tuesday") {
Assert.assertSame(
DayOfWeek.TUESDAY,
DayOfWeek.fromEpoch(given)
)
}
}
context("2000-01-05 given") {
val given = getEpoch(year = 2000, month = Calendar.JANUARY, day = 5)
it ("should be monday") {
Assert.assertSame(
DayOfWeek.MONDAY,
DayOfWeek.fromEpoch(given)
)
}
}
context("2000-01-06 given") {
val given = getEpoch(year = 2000, month = Calendar.JANUARY, day = 6)
it ("should be thursday") {
Assert.assertSame(
DayOfWeek.THURSDAY,
DayOfWeek.fromEpoch(given)
)
}
}
context("2000-01-07 given") {
val given = getEpoch(year = 2000, month = Calendar.JANUARY, day = 7)
it ("should be friday") {
Assert.assertSame(
DayOfWeek.FRIDAY,
DayOfWeek.fromEpoch(given)
)
}
}
context("2000-01-08 given") {
val given = getEpoch(year = 2000, month = Calendar.JANUARY, day = 8)
it ("should be friday") {
Assert.assertSame(
DayOfWeek.FRIDAY,
DayOfWeek.fromEpoch(given)
)
}
}
}
}) {
private fun getEpoch(year: Int, month: Int, day: Int) = Calendar.getInstance().let {
it.set(year, month, day)
it.timeInMillis
}
}
テスト失敗時のフィードバック
失敗時のフィードバックは画像のようになる。
ループを記述
次の点を意識してループを記述するパラメータ化テストを実装した。
- 1つのTestScope(ここでは
it
)に1つのAssertionになるようにする
失敗した際にそれ以降のテストケースも実行される - 何件目のテストケースで失敗したか分かるようにする
data class TestCase<T, U>(val given: T, val expected: U)
object DayOfWeekTest : Spek({
describe("DayOfWeek.fromEpoch") {
val testCaseList = listOf(
TestCase(
given = getCalendar(year = 2000, month = Calendar.JANUARY, day = 2),
expected = DayOfWeek.SUNDAY
),
TestCase(
given = getCalendar(year = 2000, month = Calendar.JANUARY, day = 3),
expected = DayOfWeek.SUNDAY
),
TestCase(
given = getCalendar(year = 2000, month = Calendar.JANUARY, day = 4),
expected = DayOfWeek.TUESDAY
),
TestCase(
given = getCalendar(year = 2000, month = Calendar.JANUARY, day = 5),
expected = DayOfWeek.MONDAY
),
TestCase(
given = getCalendar(year = 2000, month = Calendar.JANUARY, day = 6),
expected = DayOfWeek.THURSDAY
),
TestCase(
given = getCalendar(year = 2000, month = Calendar.JANUARY, day = 7),
expected = DayOfWeek.FRIDAY
),
TestCase(
given = getCalendar(year = 2000, month = Calendar.JANUARY, day = 8),
expected = DayOfWeek.FRIDAY
),
)
testCaseList.forEachIndexed { i, case ->
val (given, expected) = case
val actual = DayOfWeek.fromEpoch(given.timeInMillis)
context("${SimpleDateFormat("yyyy-mm-dd").format(given.time)} given") {
it("should be $expected [${i}]") {
Assert.assertSame(expected, actual)
}
}
}
}
}) {
private fun getCalendar(year: Int, month: Int, day: Int) = Calendar.getInstance().also {
it.set(year, month, day, 1, 1)
}
}
テスト失敗時のフィードバック
失敗時のフィードバックは画像のようになる。
これでAssertion Rouletを避けられ、どのテストケースで失敗したかある程度わかりやすいようになった。
ただ、書き方が実装者依存で統一されづらかったり、テストコード内に制御構文が現れたりするので、JUnit5みたいなフレームワークでのサポートはあると嬉しい。
コードの統一感についてはチームで認識合わせられたら十分な範囲と思うので、これでも十分かな。
Discussion