이 단계에서는 현재 탭의 레이블이 대문자로 표시되는지 확인한다.

baca545ddc8c3fa9.png

가능한 해결책은 텍스트를 찾아(find) 대문자가 존재한다고 주장(assert)하는 것이다.

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithText(RallyScreen.Accounts.name.uppercase(Locale.getDefault()))
        .assertExists()
}

그러나 실행해보면 테스트가 실패하는 것을 확인할 수 있다.😱

2abf8cb6fa32c03a.png

이 단계에서는 semantics tree를 사용하여 디버깅하는 방법을 배운다.

Semantics tree

컴포즈 테스트는 semantics tree라는 구조를 사용하여 화면에서 요소를 찾고 해당 속성을 읽는다. 이는 TalkBack과 같은 서비스에서 읽을 수 있도록 접근성 서비스에서도 사용하는 구조다.

Warning: Semantics 속성에 대한 Layout Inspector 지원은 아직 사용할 수 없다.

노드에서 printToLog 함수를 사용하여 시맨틱 트리를 인쇄할 수 있다. 테스트에 새로운 라인을 추가하자.

fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule.onRoot().printToLog("currentLabelExists")

    composeTestRule
        .onNodeWithText(RallyScreen.Accounts.name.uppercase(Locale.getDefault()))
        .assertExists() // 여전히 실패한다.
}

이제 테스트를 실행하고 Android Studio에서 Logcat을 확인하자.(currentLabelExists를 찾을 수 있다)

...com.example.compose.rally D/currentLabelExists: printToLog:
    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
     |-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
       [SelectableGroup]
       MergeDescendants = 'true'
        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
          Role = 'Tab'
          Selected = 'false'
          StateDescription = 'Not selected'
          ContentDescription = 'Bills'
          Actions = [OnClick]
          MergeDescendants = 'true'
          ClearAndSetSemantics = 'true'

전체 앱의 semantics tree는 매우 길기 때문에, 이러한 접근방식으로 격리의 편리함을 누릴 수 있음을 이해하길 바란다.

Warning: 컴포저블에는 ID가 없고, 트리에 표시된 노드 번호를 사용하여 일치시킬 수 없다. semantics 속성과 노드를 일치시키는 것이 비실용적이거나 불가능한 경우, 마지막 수단으로 hasTestTag matcher와 함께 testTag Modifier를 사용할 수 있다.

Semantics 트리를 살펴보면, 상단 앱 바의 탭인 3개의 하위 요소가 있는 SelectableGroup이 있음을 알 수 있다. “ACCOUNTS” 값을 가진 텍스트 속성이 없다. 이것이 바로 테스트를 통과하지 못한 이유다. 그러나 각 탭에 대한 컨텐트 설명(content description)이 있다. TopAppBar.kt 내부의 RallyTab 컴포저블에서 이 속성이 어떻게 설정되어 있는지 확인할 수 있다.

private fun RallyTab(text: String...)
...
    Modifier
        .clearAndSetSemantics { contentDescription = text }

이 modifier는 하위 항목에서 속성을 지우고, 자체 content description을 설정하므로 “ACCOUNTS”가 아닌 “Accounts”가 표시된다.

finder인 onNodeWithText를 onNodeWithContentDescription으로 교체하고 테스트를 다시 실행해보자.

fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithContentDescription(RallyScreen.Accounts.name)
        .assertExists()
}
3f3d9e9b90cd6cf4.png

축하합니다! 테스트를 수정하고 ComposeTestRule, 격리 테스트, finder, assertion 및 Semantics tree를 사용한 디버깅에 대해 배웠다.

하지만 나쁜 소식은 이 테스트가 그다지 유용하지 않다는 것이다! Semantics tree를 자세히 보면 탭이 선택되었는지 여부에 관계없이 세 개의 탭 모두에 대한 content description이 있다. 우리는 더 깊숙히 파고 들어가야 한다!

카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.