メインコンテンツまでスキップ
警告

このページは生成 AI によって自動生成されたページです。

Property-based testing

PreviewLab は Property-based testing との相性が良く、Field の testValues() API を使って効率的にテストを記述できます。

このページでは kotest property と Compose Preview Lab を組み合わせて Property-based testing を実装する方法を紹介します。

  • Compose Preview Lab を使ったテストについては Basic を確認してください。
  • Property-based testing については kotest のドキュメント を参照してください。

セットアップ

kotest の property 依存関係をセットアップします。 テスト環境のセットアップについては Basic を参照してください。

<kotest-version> See Kotest release note
kotlin {
sourceSets {
commonTest.dependencies {
implementation("io.kotest:kotest-property:<kotest-version>")
}
}
}

testValues() API

各 Field は testValues() メソッドを持っており、テストに有用な値のリストを返します。

interface PreviewLabField<Value> {
// ...
fun testValues(): List<Value>
}

各 Field の testValues() の挙動

FieldtestValues() の内容
デフォルト[initialValue]
BooleanField[true, false]
SelectableField選択可能なすべてのオプション
EnumFieldすべての enum 値
NullableFieldbaseField.testValues() + [null]
CombinedFieldサブフィールドの testValues() の直積 (全ての組み合わせ)
  • NullableField など、他のフィールドと組み合わせるタイプの Field は、ベースとなるフィールドの testValues() と自身の仕様を組み合わせて値を返します。
  • 被りがある場合は重複しないように自動で取り除かれます。
// Preview 内
val str: String? =
fieldValue {
SelectableField("str", listOf("a", "b", "c"))
.nullable()
}

// Test
val strField by state.field<String?>("str")
strField.testValues() // will be: ["a", "b", "c", null]

kotest-property との連携

kotest-property の Arb/Exhansive と組み合わせることで、ランダムな値と testValues() のエッジケースを両方テストできます。

import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.plusEdgecases
import io.kotest.property.forAll

@Test
fun `IntField should update preview when value changes`() = runDesktopComposeUiTest {
val state = PreviewLabState()
setContent { TestPreviewLab(state) { MyPreview() } }

val countField by state.field<Int>("Count")

// ランダムな int 値 + testValues() のエッジケースをテスト
forAll(Arb.int().plusEdgecases(countField.testValues())) { intValue ->
countField.value = intValue
awaitIdle()

onNodeWithText("Count: $intValue")
.assertExists()
true
}
}

様々な Field のテスト例

BooleanField

val enabledField by state.field<Boolean>("Enabled")

forAll(Arb.boolean().plusEdgecases(enabledField.testValues())) { boolValue ->
enabledField.value = boolValue
awaitIdle()
// TODO assert
}

SelectableField / EnumField

val themeField by state.field<Theme>("Theme")

// testValues() にはすべての選択肢が含まれる
forAll(Arb.of(themeField.testValues()).plusEdgecases(themeField.testValues())) { theme ->
themeField.value = theme
awaitIdle()
// TODO assert
}

NullableField

val userNameField by state.field<String?>("User Name")

forAll(Arb.string(1..20).orNull().plusEdgecases(userNameField.testValues())) { userName ->
userNameField.value = userName
awaitIdle()

val expectedText = userName ?: "No user name"
// TODO assert
}

カスタム testValues の追加

既存の Field に対して .withTestValues() 拡張関数を使って、追加のテスト値を指定できます。

@Preview
@Composable
fun MyPreview() = PreviewLab {
val count = fieldValue {
IntField("Count", 0)
.withTestValues(-1, 0, 1, Int.MAX_VALUE, Int.MIN_VALUE)
}
// ...
}

カスタムフィールドの testValues()

カスタムフィールドを作成している場合は必要に応じて testValues() メソッドをオーバーライドする必要があります。

open class MyField(...) : MutablePreviewLabField<MyValue>(...) {
// ...
override fun testValues(): List<MyValue> =
super.testValues() +
listOf(testMyValue1, testMyValue2, ...)
testValues() をオーバーライドする際は必ず super.testValues() に追加する形で追加の値を提供してください

super.testValues() を呼び出さないと親要素の testValues() が無視されてしまいます。