mcpField
A
asserting-node-state-and-text

asserting-node-state-and-text

github.com/skydovesテスト・品質33964 回インストール22 週間前に更新
リポジトリ

asserting-node-state-and-text について

Compose テストで、特定したセマンティクスノードのプロパティを検証するスキルです。型付きアサーションの選び方に加え、表示状態、コレクション検証、カスタムマッチャー、waitUntil 用のブール述語を説明します。

主な機能

  1. 01ノードのプロパティに応じた型付きアサーション
  2. 02assertExists と assertIsDisplayed の違い
  3. 03コレクションの件数・任意・全件検証
  4. 04カスタムマッチャーによるアサーション
  5. 05waitUntil でのブール値による表示確認

活用シーン

  1. 01Compose のトグル状態やテキストの確認
  2. 02ノードの存在だけでなく表示状態の検証
  3. 03コレクション件数や選択状態の検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/skydoves/android-testing-skills --skill asserting-node-state-and-text

インストール後、Claude Code で/asserting-node-state-and-textを入力して読み込まれたか確認します。

詳細

ノードの状態とテキストをアサートする — fetchSemanticsNode ではなくフレームワークを通じて検証する

ファインダーが SemanticsNodeInteraction を解決したら、次のステップはアサートです。Compose には、ほぼすべてのセマンティックプロパティに対応する型付きアサーションが用意されています。残りは汎用の assert(matcher) で対応できます。このスキルでは、適切なアサーションの選び方、「存在する」と「表示されている」の違い、ブール値の述語を waitUntil の中で使う場面を説明します。

このスキルを使用する場面

  • Switch がオン、Checkbox が未チェック、Button が有効、Text が期待する文字列と一致していることを検証したい場合。
  • assertIsDisplayed と assertExists の違いについて尋ねられた場合(前者は画面上に表示されていること、後者はツリー内に存在することを検証します)。
  • 型付きの拡張関数がないプロパティに対して、assert(matcher) でカスタムアサーションを行いたい場合。
  • コレクションを検証したい場合:「項目がちょうど 3 つある」「少なくとも 1 つが選択されている」「すべてが有効になっている」。
  • 型付きアサーションを使わず、テストで node.config[...] を直接比較している場合。

このスキルを使用しない場面

  • 適切なノードを特定できない場合 — ../../finders/finding-nodes-by-tag-text-content/SKILL.md を参照してください。
  • アサーションが幾何学的なもの(幅、高さ、位置)の場合 — ./asserting-bounds-and-dimensions/SKILL.md を参照してください。
  • カスタムの SemanticsMatcher が必要な場合 — ../../finders/composing-semantics-matchers/SKILL.md を参照してください。
  • 状態が非同期に変化し、アサーションのタイミングが重要な場合 — ../../synchronization/synchronizing-with-idle/SKILL.md を参照してください。

前提条件

  • 動作する ComposeTestRule / ComposeUiTest があること。../../setup/configuring-test-dependencies/SKILL.md を参照してください。
  • onNode* / onAllNodes* / ナビゲーターを通じてノードが特定されていること。
  • assertIsDisplayed のセマンティクスでは、ノードがコンポーズされ、配置され、クリップ後に画面上へ少なくとも一部が表示されている必要があります(Assertions.kt:30-39)。

ワークフロー

  • 1. 質問の種類に応じて選びます。 開発者の質問を、1 つのアサーション呼び出しに対応付けます。特に記載がない限り、一覧の API はすべて commonMain/.../Assertions.kt にあります。

    質問APIファイル:行
    ツリー内にありますか?assertExists(errorMessageOnFail)SemanticsNodeInteraction.kt:124-127
    存在しなくなりましたか?assertDoesNotExist()SemanticsNodeInteraction.kt:94-110
    SubcomposeLayout に保持された子ですか?assertIsDeactivated()SemanticsNodeInteraction.kt:137-148
    ユーザーに表示されていますか?assertIsDisplayed() / assertIsNotDisplayed()Assertions.kt:32-53
    操作できますか?assertIsEnabled() / assertIsNotEnabled()Assertions.kt:60-67
    トグルはオンですか?assertIsOn() / assertIsOff() / assertIsToggleable()Assertions.kt:74-81, 103
    ラジオボタン/タブは選択されていますか?assertIsSelected() / assertIsNotSelected() / assertIsSelectable()Assertions.kt:88-110
    フォーカスされていますか?assertIsFocused() / assertIsNotFocused()Assertions.kt:117-124
    これらの文字列と一致しますか?assertTextEquals(vararg, includeEditableText=true)Assertions.kt:181-185
    この文字列を含んでいますか?assertTextContains(value, substring=false, ignoreCase=false)Assertions.kt:204-208
    Content description はこれらと一致しますか?assertContentDescriptionEquals(vararg)Assertions.kt:139-141
    Content description にこの文字列が含まれていますか?assertContentDescriptionContains(value, substring, ignoreCase)Assertions.kt:158-163
    StateDescription は X と一致しますか?assertValueEquals(value)Assertions.kt:216-217
    ProgressBar はこの位置にありますか?assertRangeInfoEquals(rangeInfo)Assertions.kt:225-227
    クリックアクションがありますか?assertHasClickAction() / assertHasNoClickAction()Assertions.kt:234-243
    その他assert(matcher, messagePrefixOnError = null)Assertions.kt:254-267
  • 2. assertIsDisplayed と assertExists の使い分けは慎重に行います。 ノードがセマンティクスツリー内にある(コンポーズされている)ことを確認する場合は、assertExists() を使います。ノードがコンポーズされ、配置され、クリッピング後に少なくとも一部が表示されていることを確認する場合は、assertIsDisplayed() を使います(Assertions.kt:30-39)。まだスクロールして表示されていない LazyColumn の項目では、assertExists() は失敗する場合(項目がコンポーズされていない)も、成功する場合(項目はコンポーズされているものの画面外にある。プリフェッチの有無によります)もあります。「ユーザーに見えている」ことを確認するには、assertIsDisplayed() が適切です。

  • 3. fetchSemanticsNode().config[...] ではなく、型付きアサーションを使います。 型付きアサーションはマッチャーライブラリに処理を委譲し、失敗したプロパティ名とノード情報を含む、フレームワーク形式のエラーを出力します(Assertions.kt:254-267 — 汎用の assert は buildGeneralErrorMessage(errorMessageOnFail, selector, node) を構築します)。設定を直接読み取ると、このエラー報告を迂回して、自動添付されるノード情報も得られません。

  • 4. コレクションには、コレクション型のアサーションを使います。 要素数の確認には assertCountEquals(expectedSize)(Assertions.kt:276-292)、少なくとも 1 つが一致することの確認には assertAny(matcher)(空の場合は失敗します — Assertions.kt:300-312)、すべてのノードが一致することの確認には assertAll(matcher)(空の場合は成功します — Assertions.kt:323-339)を使います。

  • 5. waitUntil の述語には、例外をスローするアサーションではなく、真偽値を返す API を使います。 isDisplayed() は Boolean を返すため(Assertions.kt:351)、rule.waitUntil { node.isDisplayed() } に最適です。isNotDisplayed() はその逆です(Assertions.kt:361)。どちらもファインダーに一致するノードが複数ある場合は例外をスローしますが、0 個の場合はスローしません。関連項目:../../synchronization/synchronizing-with-idle/SKILL.md。

  • 6. 型付きアサーションがない単発のプロパティ確認には、assert(matcher) を使います。 ../../finders/composing-semantics-matchers/SKILL.md にある既成のマッチャー、または組み合わせたマッチャーと併用します。messagePrefixOnError ラムダは、このアサーションがより大きな操作の前提条件となる場合に、コンテキストを追加するためのものです。

パターン

パターン:ユーザー向けの確認では assertExists より assertIsDisplayed を優先
// WRONG
@Test
fun submit_isVisibleAfterError() {
    rule.setContent { CheckoutScreen(state = state) }
    rule.onNodeWithTag(SubmitTag).assertExists()
}
// WRONG because: assertExists passes for nodes that are composed but off-screen, behind a
// dialog, or measured to zero size. The test does not prove the user actually sees the button.
// RIGHT
@Test
fun submit_isVisibleAfterError() {
    rule.setContent { CheckoutScreen(state = state) }
    rule.onNodeWithTag(SubmitTag).assertIsDisplayed()
}
パターン:手動で設定を読み取る代わりに、型付きの assertIsOn を使う
// WRONG
@Test
fun darkMode_switch_isOn() {
    rule.setContent { SettingsScreen() }
    val node = rule.onNodeWithTag(DarkModeSwitchTag).fetchSemanticsNode()
    assertEquals(ToggleableState.On, node.config[SemanticsProperties.ToggleableState])
}
// WRONG because: bypasses the framework's error reporting. Failure prints a JUnit assertEquals
// diff with no node dump, no selector description, no hint about which screen the failure
// happened on.
// RIGHT
@Test
fun darkMode_switch_isOn() {
    rule.setContent { SettingsScreen() }
    rule.onNodeWithTag(DarkModeSwitchTag).assertIsOn()
}

assertIsOn は assert(isOn()) です(Assertions.kt:74)。isOn() は expectValue(SemanticsProperties.ToggleableState, ToggleableState.On) です(Filters.kt:61-62)。エラーにはプロパティ名が表示され、ノードの内容も自動的に出力されます。

パターン:assertTextEquals(可変長引数)で、統合された Text と EditableText を扱う
// production:
TextField(value = "hello", onValueChange = {}, label = { Text("Name") },
    modifier = Modifier.testTag(NameFieldTag))

// test:
rule.onNodeWithTag(NameFieldTag).assertTextEquals("Name", "hello")

assertTextEquals(vararg) は、SemanticsProperties.Text と、デフォルトでは SemanticsProperties.EditableText を合わせた順不同の集合を照合します(Assertions.kt:181-185 → Filters.kt:274-293)。比較から編集可能なテキストを除外するには、assertTextEquals("Name", includeEditableText = false) を使います。

パターン:onAllNodes(...).onFirst().assertExists() の代わりに assertCountEquals を使う
// WRONG
rule.onAllNodesWithTag(ItemTag).onFirst().assertExists()
// WRONG because: only proves there is at least one item. Drift to zero items causes
// "no node matched" instead of a count error; drift to many is silently accepted.
// RIGHT
rule.onAllNodesWithTag(ItemTag).assertCountEquals(3)
パターン:型付きアサーションがないプロパティには assert(matcher) を使う
@Test
fun row_hasPriorityOne() {
    rule.setContent { TaskList(tasks = tasks) }

    rule.onNodeWithTag(TaskRowTag)
        .assert(SemanticsMatcher.expectValue(PriorityKey, 1))
}

PriorityKey はカスタムの SemanticsPropertyKey<Int> で、Modifier.semantics { priority = … } を使って設定します。マッチャーの合成については、../../finders/composing-semantics-matchers/SKILL.md を参照してください。

パターン:assertAny(空の場合は失敗)と assertAll(空の場合は成功)
// "at least one row is selected" — must have at least one row
rule.onAllNodesWithTag(RowTag).assertAny(isSelected())

// "every row is enabled" — accepts zero rows (passes vacuously)
rule.onAllNodesWithTag(RowTag).assertAll(isEnabled())

空のコレクションに対して assertAny は AssertionError("Failed to assertAny … no node matched") をスローします(Assertions.kt:305-307)。assertAll は空のコレクションに対して正常に終了します(Assertions.kt:323-339)。意図に応じて使い分けてください。

パターン:waitUntil 内で isDisplayed() を使う
// WRONG
rule.waitUntil { rule.onNodeWithTag(SnackbarTag).assertIsDisplayed(); true }
// WRONG because: assertIsDisplayed throws on the first poll where the snackbar isn't yet
// shown. waitUntil expects a boolean — predicates that throw will fail-fast.
// RIGHT
rule.waitUntil(timeoutMillis = 2_000) {
    rule.onAllNodesWithTag(SnackbarTag).fetchSemanticsNodes().isNotEmpty() &&
        rule.onNodeWithTag(SnackbarTag).isDisplayed()
}
rule.onNodeWithTag(SnackbarTag).assertTextContains("Saved")

isDisplayed() は一致するノードが 0 個の場合は false を返し、複数個の場合にのみ例外をスローします(Assertions.kt:343-351)。関連項目:waitUntil と mainClock.advanceTimeUntil の使い分けについては、../../synchronization/synchronizing-with-idle/SKILL.md を参照してください。

パターン:SubcomposeLayout に保持された子には assertIsDeactivated を使う
// LookaheadLayout / SubcomposeLayout may retain a previously composed slot for reuse.
// To verify the slot is currently deactivated (kept, not active):
rule.onNodeWithTag(RetainedSlotTag).assertIsDeactivated()

assertIsDeactivated は非アクティブ化されたノードを除外せずにノードを取得し(SemanticsNodeInteraction.kt:137-148)、node.layoutInfo.isDeactivated を確認します。

必須ルール

  • 必須: fetchSemanticsNode().config[...] より、型付きアサーション(assertIsOn、assertIsEnabled、assertTextEquals など)を使ってください。フレームワークのエラーメッセージには、セレクター、ノードの内容、失敗した条件が含まれます。
  • 必須: 条件が「ユーザーにこれが見えること」である場合は assertIsDisplayed() を使ってください。条件が「これがセマンティクスツリー内にあること」である場合に限り(画面上に表示されている必要はなく、統合されていないツリー内での存在確認など)、assertExists() を使ってください。
  • 必須: waitUntil { … } ブロック内では、ブール値を返す isDisplayed()/isNotDisplayed() を使ってください。waitUntil 内で例外をスローする assertIs* を呼び出してはいけません。
  • 必須: コレクションの要素数確認には assertCountEquals を使ってください。「唯一の要素」を示すために [0] でインデックス指定してはいけません。
  • 禁止: 合成したマッチャーで済む場合に、連鎖呼び出しの中で同じアサーションを繰り返してはいけません。../../finders/composing-semantics-matchers/SKILL.md を参照してください。
  • 推奨: アサーションの前に、タグを基準にした検索を優先してください。Skydoves の注目ポイント #1。

検証

  • node.config[...] を直接読み取るアサーションがないこと。すべてのプロパティ確認が assertIs*/assertTextEquals/assert(matcher) を通じて行われていること。
  • ユーザーに見えるかどうかの確認には assertIsDisplayed が使われ、画面上の表示が関係ない場合に限って assertExists が使われていること。
  • waitUntil { … } ブロック内では isDisplayed()/isNotDisplayed()/fetchSemanticsNodes().size が使われ、例外をスローする assertIs* 呼び出しがないこと。
  • assertAny/assertAll の選択が、空のコレクションに対する意図した動作と一致していること。
  • ./gradlew :app:connectedDebugAndroidTest(デバイス)または :app:testDebugUnitTest(ホスト)が成功すること。
  • 失敗メッセージに問題のあるセマンティクスプロパティ名が含まれていること(型付きアサーションが使われている証拠)。

参考資料

コメント