onNodeWithTag, onNodeWithText, onNodeWithContentDescription을 통해 UI 노드를 찾을 때의 한계점
onNodeWith- 구문을 사용해 UI 노드를 찾게 되면, 특정한 조건 하나만을 가진 UI 노드를 찾게 된다. 만약 같은 값을 공유하는 UI 노드가 있다면 둘 중 하나만이 선택되며, 그 둘을 구분할 수 있는 방법은 없다. 예를 들어 다음과 같이 같은 Smile이라는 이름을 가진 EmojiText Composable이 두 개 있다고 해보자.
class OnNodeTest {
@get:Rule
val composeRule = createComposeRule()
@Test
fun onNodeWithProblem() {
// Given
var isSecondSmileButtonClicked = false
composeRule.setContent {
Column() {
EmojiText(
emoji = "😎",
content = "Smile"
)
EmojiText(
modifier = Modifier.clickable {
isSecondSmileButtonClicked = true
},
emoji = "😃",
content = "Smile"
)
}
}
// When
composeRule.onNodeWithText("Smile").performClick()
// ...
}
}
만약 여기서 onNodeWithText를 통해 UI 노드를 찾는다면, 둘 중 하나(일반적으로 앞의 노드)가 찾아질 것이다. 만약 앞의 UI 노드가 선택됐다면, 뒤의 노드를 클릭하는 액션을 테스트하고 싶다면 방법이 없다.
onNode를 통해 UI 노드를 정확하게 찾기
이를 해결하기 위해서는 ComposeContentTestRule 객체가 제공하는 onNode함수를 통해 UI 노드를 찾으면 된다. onNode 함수를 사용하면 더이상 특정한 값으로 UI 노드를 찾는 것이 아닌, SemanticsMatcher이라는 객체를 통해 해당 조건을 만족하는 UI 노드를 찾게돼 더욱 고도화된 UI 노드 찾기가 가능해진다.
fun onNode(
matcher: SemanticsMatcher,
useUnmergedTree: Boolean = false
): SemanticsNodeInteraction
SemanticsMatcher을 사용하는 방법은 간단하다. SemanticsMatcher 객체는 has- 구분을 통해 만들 수 있으며, 만약 둘 이상의 조건이 중복되어야 한다면 and 를 통해 둘 이상의 SemanticsMatcher 객체를 합칠 수 있다.
예를 들어 뒤의 EmojiText는 "Smile" 문자열 말고도, click 액션을 가지고 있으므로, hasText("Smile")과 hasClickAction()을 모두 만족하는 노드를 찾으면 된다. 따라서, 이 두가지를 and 를 사용해 연결하면 뒤의 EmojiText 노드를 찾을 수 있다.
class OnNodeTest {
@get:Rule
val composeRule = createComposeRule()
@Test
fun onNodeWithFixProblem() {
// Given
var isSecondSmileButtonClicked = false
composeRule.setContent {
Column() {
EmojiText(
emoji = "😎",
content = "Smile"
)
EmojiText(
modifier = Modifier.clickable {
isSecondSmileButtonClicked = true
},
emoji = "😃",
content = "Smile"
)
}
}
// When
composeRule
.onNode(
hasText("Smile")
.and(hasClickAction())
)
.performClick()
// Then
assertTrue(isSecondSmileButtonClicked)
}
}
이제 이 테스트를 실행해보면 다음 화면과 같이 테스트가 통과되는 것을 볼 수 있다.