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()
}이 요구사항은 앱의 나머지 부분이 아니라 Compose 계층구조에만 적용됩니다.
자동 동기화 사용 중지
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와 동기화되지 않는 백그라운드 작업을 실행하는 경우 추가 유휴 리소스를 등록해야 하지 않으면 조치를 취할 필요가 없습니다.
이 API는 Espresso의 유휴 리소스와 매우 유사하며 테스트 대상이 유휴 상태인지 아니면 사용 중인지 나타냅니다. Compose 테스트 규칙을 사용하여
구현을 등록합니다 IdlingResource.
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의 상태만 확인함).
애니메이션 테스트 최적화
When testing high-fidelity animations, you often need to disable auto-advance
and manually step through frames to assert intermediate UI states. For these
specific frame-by-frame loops, use the runWithoutImplicitWait method to
execute your assertions. Standard node queries (like onNodeWithTag or
fetchSemanticsNode) trigger implicit synchronizations that are redundant
when you are manually controlling the clock, so bypassing them significantly
speeds up your test runtimes.
Usage guidelines
- Manual clock management: Use this API when
mainClock.autoAdvanceis set tofalseand the UI is in a known, stable state for the current frame. - UI thread execution: To ensure the stability of the UI tree, call
runWithoutImplicitWaiton the UI thread, such as withrunOnUiThread. Running it off the UI thread exposes your test to race conditions and stale state reads. - Read-only assertions: The block should strictly contain read-only assertions. Any actions that mutate state should be performed outside of this block.
Example
@Test fun runWithoutImplicitWaitSample() = runComposeUiTest { setContent { MainScreen() } mainClock.autoAdvance = false // Trigger an animation onNodeWithText("Start Animation").performClick() // Step through the animation frame-by-frame while (hasPendingWork()) { mainClock.advanceTimeByFrame() waitForIdle() runOnUiThread { // Suppress implicit synchronization inside this block to avoid redundant // waits on each node query, making the frame assertions execute much faster. runWithoutImplicitWait { val box1 = onNodeWithTag("Box1").fetchSemanticsNode() val box2 = onNodeWithTag("Box2").fetchSemanticsNode() val box3 = onNodeWithTag("Box3").fetchSemanticsNode() // Assert the exact intermediate state of all three properties for this frame assert(box1.boundsInRoot.right <= box2.boundsInRoot.left) assert(box2.boundsInRoot.right <= box3.boundsInRoot.left) } } } }
기본 스레드 동기화
이제 Compose 테스트에서 기본 스레드 동기화를 지원하므로 기본 스레드에서 직접 waitForIdle 및 Compose UI 작업과 어설션을 안전하게 호출할 수 있습니다.
이전에는 Compose 테스트에서 2개 스레드 모델을 엄격하게 적용했습니다. 테스트 실행은 백그라운드 테스트 스레드에서 발생하고 UI 업데이트는 기본 스레드에서 발생했습니다. 기본 스레드에서 waitForIdle 또는 runOnIdle과 같은 동기화 메서드를 호출하면 (예: runOnUiThread 블록 내부) 프레임워크에서 기본 스레드 동기화를 방지하기 위해 엄격한 스레드 검사를 적용하므로 IllegalStateException이 발생합니다.
기본 스레드 동기화를 사용 설정하면 이제 Compose 테스트 프레임워크에서 기본 스레드에서 차단 호출이 이루어지더라도 클록을 앞당기고 대기 중인 작업을 처리할 수 있습니다.
기본 스레드 동기화를 사용하는 경우
백그라운드 스레드에서 테스트를 유지하는 것은 순수 Compose 테스트의 표준이지만 몇 가지 특정 시나리오에서는 기본 스레드 동기화가 매우 유리합니다.
- 복잡한 뷰 상호 운용성: Compose와 기존 Android 뷰가 모두 포함된 하이브리드 UI를 테스트할 때 뷰를 조작하려면 기본 스레드에서 실행해야 하는 경우가 많습니다. 이제 스레드 컨텍스트를 지속적으로 전환하지 않고도 뷰와 상호작용하고 Compose 노드에서 순차적으로 어설션할 수 있습니다.
- 동기식 상태 변경: 아키텍처가 엄격하게 기본 스레드에 바인딩된 상태 홀더에 의존하는 경우 이제 기본 스레드를 벗어나지 않고도 상태를 변경하고 Compose UI가 안정화될 때까지 즉시 기다릴 수 있습니다.
- 커스텀 테스트 실행기: 커스텀 테스트 인프라를 빌드하거나 테스트 실행기가 기본적으로 기본 스레드에서 실행되는 환경을 활용하는 경우 이제 Compose 테스트가 백그라운드 스레드 위임을 요구하지 않고 깔끔하게 실행됩니다.
예
이전에는 기본 스레드에서 동기화가 엄격하게 금지되었으므로 개발자는 백그라운드 테스트 실행기 스레드와 UI 스레드 간에 앞뒤로 이동해야 했으므로 테스트가 분리되었습니다.
@Test fun testBidirectionalInteropUIUpdates_old() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() } // Jump to Test Thread to verify state settles inside compose composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() // Jump back to Main Thread to verify target view state settles scenario.onFragment { fragment -> assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
기본 스레드 동기화를 사용 설정하면 Compose 및 뷰 계층구조의 어설션을 동일한 블록에서 실행할 수 있습니다.
@Test fun testBidirectionalInteropUIUpdates_new() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
조건 대기
데이터 로드 또는 Android의
측정 또는 그리기(즉 Compose 외부의 측정 또는 그리기)와 같은 외부 작업에 종속되는 모든 조건은
더 일반적인 개념(예: waitUntil())을 사용해야 합니다.
composeTestRule.waitUntil(timeoutMs) { condition }
composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)
composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)
composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)
composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)
추가 리소스
- Android에서 앱 테스트: 기본 Android 테스트 방문 페이지에서는 테스트 기본사항과 기법을 더 폭넓게 살펴볼 수 있습니다.
- 테스트 기본사항: Android 앱 테스트의 기본 개념에 관해 자세히 알아보세요.
- 로컬 테스트: 일부 테스트는 자체 워크스테이션에서 로컬로 실행할 수 있습니다.
- 계측 테스트: 계측 테스트도 실행하는 것이 좋습니다. 즉, 기기에서 직접 실행되는 테스트입니다.
- 지속적 통합: 지속적 통합을 사용하면 테스트를 배포 파이프라인에 통합할 수 있습니다.
- 다양한 화면 크기 테스트: 사용자에게 제공되는 기기가 많으므로 다양한 화면 크기를 테스트해야 합니다.
- Espresso: 뷰 기반 UI를 위한 것이지만 Espresso 지식은 Compose 테스트의 일부 측면에서 여전히 유용할 수 있습니다.