개요
최근 컴포즈를 많이 사용 함에 따라 테스트 코드를 짤 수 있어야 된다는 생각이 들고 있습니다 물론 Preview를 통해 잘 동작하는지 확인할 순 있겠지만 전반적인 프로세스가 잘 동작하는 파악하기란 어려운 일이라 생각되어 이번 기회에 정리해볼까 합니다.
시맨틱 (Semantics)
기존 Android Espresso에서 id와 해당 특성을 가진 컴포넌트를 찾을 수 있었습니다. 하지만 컴포즈에서는 id를 명시하지 않기 때문에 시멘틱을 통해 UI에 의미를 부여합니다. 단일 컴포저블에서 전체 화면에 이르기까지의 무엇이든 의미할 수 있으며 시맨틱 트리는 UI 계층 구조와 함께 생성되고 UI 계층 구조를 형성합니다.
ComposeTestRule
ComposeTestRule은 component, Activity를 시작하는 방법 및 finder, actions, assertions 등을 제공하는 인터페이스입니다. 특정 액티비티를 지정하고 싶다면 createAndroidComposeRule<ACTIVITY_NAME>() 을 통해 생성할 수 있습니다.
class MyComposeTest {
/**
* use createAndroidComposeRule<YourActivity>() if you need access to an activity
*/
@get:Rule
val composeTestRule = createComposeRule()
}
컴포즈 테스트 API
Finder
파인더를 사용하면 요소(또는 시맨틱 트리의 노드)를 하나 이상 선택하여 어설션을 만들거나 작업을 실행할 수 있습니다.
Assertions
어설션은 요소가 있는지 또는 특정 속성을 보유하는지 확인하는 데 사용됩니다.
Actions
작업은 클릭이나 기타 동작과 같은 시뮬레이션된 사용자 이벤트를 요소에 삽입합니다.
Matchers
매처는 테스트하고 싶은 컴포넌트를 찾을 수 있는 기능을 제공합니다.
Compose 시멘틱 트리 구조 출력 방법
일부 노드는 하위 요소의 시맨틱 정보를 병합합니다. 예를 들어 다음과 같이 텍스트 요소가 두 개 있는 버튼은 라벨을 병합합니다.
MyButton {
Text("Hello")
Text("World")
}
다음과 같이 테스트에서 printToLog()를 사용하여 시맨틱 트리를 표시할 수 있습니다.
composeTestRule.onRoot().printToLog("TAG")
이 코드는 다음 출력을 생성합니다.
Node #1 at (...)px
|-Node #2 at (...)px
Role = 'Button'
Text = '[Hello, World]'
Actions = [OnClick, GetTextLayoutResult]
MergeDescendants = 'true'
병합되지 않은 트리가 될 요소의 노드를 일치시켜야 한다면 다음과 같이 useUnmergedTree를 true로 설정하면 됩니다.
composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")
이 코드는 다음 출력을 생성합니다.
Node #1 at (...)px
|-Node #2 at (...)px
OnClick = '...'
MergeDescendants = 'true'
|-Node #3 at (...)px
| Text = '[Hello]'
|-Node #5 at (83.0, 86.0, 191.0, 135.0)px
Text = '[World]'
useUnmergedTree 매개변수는 모든 파인더에서 사용할 수 있습니다. 예를 들어 여기서는 onNodeWithText 파인더에서 사용됩니다.
composeTestRule
.onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()
동기화
Compose 테스트는 기본적으로 UI와 동기화됩니다.
ComposeTestRule을 통해 어설션이나 작업을 호출하면 테스트가 미리 동기화되고 UI 트리가 유휴 상태가 될 때까지 기다립니다.
일반적으로 별도의 조치를 취할 필요가 없습니다. 하지만 몇 가지 극단적 사례에 관해 알아야 합니다.
테스트가 동기화되면 Compose 앱이 가상 클록을 사용하여 시간을 앞당깁니다. 즉 Compose 테스트가 실시간으로 실행되지 않으므로 최대한 빨리 통과할 수 있습니다.
하지만 테스트를 동기화하는 메서드를 사용하지 않는 경우 리컴포지션이 발생하지 않으며 UI가 일시중지된 것으로 표시됩니다.
@Test
fun counterTest() {
val myCounter = mutableStateOf(0) // State that can cause recompositions
var lastSeenValue = 0 // Used to track recompositions
composeTestRule.setContent {
Text(myCounter.value.toString())
lastSeenValue = myCounter.value
}
myCounter.value = 1 // The state changes, but there is no recomposition
// Fails because nothing triggered a recomposition
assertTrue(lastSeenValue == 1)
// Passes because the assertion triggers recomposition
composeTestRule.onNodeWithText("1").assertExists()
}
자동 동기화 사용 중지
assertExists()와 같은 ComposeTestRule을 통해 어설션이나 작업을 호출하면 테스트가 Compose UI와 동기화됩니다.
경우에 따라 이 동기화를 중지하고 클록을 직접 제어해야 할 수도 있습니다.
예를 들어 UI가 계속 사용 중일 때 애니메이션의 정확한 스크린샷을 캡처하는 시간을 제어할 수 있습니다. 자동 동기화를 사용 중지하려면 mainClock의 autoAdvance 속성을 false로 설정하세요.
composeTestRule.mainClock.autoAdvance = false
일반적으로 그런 다음에 직접 시간을 앞당깁니다. advanceTimeByFrame()을 사용하여 정확히 한 프레임을 앞당기거나 advanceTimeBy()를 사용하여 특정 기간만큼 앞당길 수 있습니다.
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
유휴 리소스
Compose는 모든 작업과 어설션이 유휴 상태에서 실행되도록 테스트와 UI를 동기화하여 필요에 따라 기다리거나 클록을 앞당길 수 있습니다. 하지만 결과가 UI 상태에 영향을 미치는 일부 비동기 작업은 테스트가 인식하지 못하는 동안 백그라운드에서 실행할 수 있습니다.
테스트에서 이 유휴 리소스를 만들고 등록하여 테스트 중인 앱이 사용 중인지 아니면 유휴 상태인지 파악할 때 고려할 수 있습니다. 예를 들어 Espresso 또는 Compose와 동기화되지 않는 백그라운드 작업을 실행하는 경우 추가 유휴 리소스를 등록해야 하지 않으면 조치를 취할 필요가 없습니다.
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
수동 동기화
경우에 따라 Compose UI를 테스트의 다른 부분 또는 테스트 중인 앱과 동기화해야 합니다.
waitForIdle은 Compose가 유휴 상태가 될 때까지 기다리지만 autoAdvance 속성에 종속됩니다.
composeTestRule.mainClock.autoAdvance = true // default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for Idling Resources to become idle
두 경우 모두 waitForIdle은 대기 중인 그리기 및 레이아웃 단계도 기다립니다.
또한 advanceTimeUntil()을 사용하여 특정 조건이 충족될 때까지 클록을 앞당길 수 있습니다.
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
지정된 조건은 클록의 영향을 받을 수 있는 상태를 확인해야 합니다(조건은 Compose의 상태만 확인함). Android의 측정 또는 그리기에 종속되는 모든 조건(즉 Compose 외부의 측정 또는 그리기)은 waitUntil()과 같이 더 일반적인 개념을 사용해야 합니다.
composeTestRule.waitUntil(timeoutMs) { condition }
경고: 경우에 따라 테스트에 waitUntil API 대신 외부 CountDownLatch와 같은 메커니즘을 사용하면 테스트 클록이 앞당겨지지 않으므로 예기치 않은 동작이 발생할 수 있습니다.
맞춤 시맨틱 속성
// Creates a Semantics property of type boolean
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey
이제 semantics 수정자를 사용하여 이 속성을 사용할 수 있습니다.
val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
modifier = Modifier.semantics { pickedDate = datePickerValue }
)
테스트에서 SemanticsMatcher.expectValue를 사용하여 속성 값을 어설션할 수 있습니다.
composeTestRule
.onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
.assertExists()
UiAutomator와의 상호 운용성
기본적으로 컴포저블은 표시된 텍스트, 콘텐츠 설명 등을 통해서만 UiAutomator에 액세스할 수 있습니다.
Modifier.testTag를 사용하는 모든 컴포저블에 액세스하려면 특정 컴포저블 하위 트리에 testTagAsResourceId 시맨틱 속성을 사용 설정해야 합니다.
이 동작을 사용 설정하면 스크롤 가능한 컴포저블과 같이 다른 고유 핸들이 없는 컴포저블(예: LazyColumn)에 유용합니다.
UiAutomator에서 Modifier.testTag와 함께 중첩된 모든 컴포저블에 액세스할 수 있도록 컴포저블 계층 구조에서 한 번만 사용 설정할 수 있습니다.
Scaffold(
// Enables for all composables in the hierarchy.
modifier = Modifier.semantics {
testTagsAsResourceId = true
}
){
// Modifier.testTag is accessible from UiAutomator for composables nested here.
LazyColumn(
modifier = Modifier.testTag("myLazyColumn")
){
// content
}
}
Modifier.testTag(tag)를 포함하는 모든 컴포저블은 resourceName과 동일한 tag로 By.res(resourceName)을 사용하여 액세스할 수 있습니다.
val device = UiDevice.getInstance(getInstrumentation())
val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// some interaction with the lazyColumn
참고: 이 기능은 Jetpack Compose 버전 1.2.0-alpha08 이상에서 사용할 수 있습니다.
정리
지금까지 이론에 대해서 정리를 해봤습니다. 어떻게 보면 Espresso를 공부할 때랑 비슷한 느낌을 받기도 했는데 실제 CodeLab를 따라가보면서 실전편도 같이 정리하면 이해하기 쉬울 것 같습니다!
'Android' 카테고리의 다른 글
[Android] 앱 아키텍처 UI레이어와 UiState (0) | 2023.09.28 |
---|---|
[Android-Test] Android Compose Test 도입하기 - 실전 (0) | 2023.09.27 |
[Android-Test] Android Test코드 작성하기5 - Hilt (0) | 2023.09.26 |
[Android-Test] Android Test코드 작성하기4 - Flow (0) | 2023.09.26 |
[Android-Test] Android Test코드 작성하기3 - Coroutines (0) | 2023.09.25 |
댓글