Compose UI 테스트를 진행할 때, 구현 코드에서 UI 변경사항이나 코루틴 같은 비동기 코드를 제대로 다루지 않으면 원하는 타이밍에 테스트 코드를 실행할 수 없습니다. 그렇기 때문에 이러한 동기화를 다루는 여러 개의 방법들을 지원합니다. Compose 테스트에서 사용 가능한 동기화 방법들에 대해 설명하겠습니다.
MessageQueue에 등록된 메시지/작업만 추적. 예를 들어 View의 그리기, UI 이벤트 처리 등은 자동으로 동기화 대상. service 동작 등은 인식 불가능IdlingRegistry에 등록된 모든 IdlingResource를 확인. 각 IdlingResource의 isIdleNow()를 호출해 idle 여부 판정. isIdleNow()가 false라면 Espresso는 대기. 리소스가 idle이 될 때 ResourceCallback.onTransitionToIdle()을 호출하여 Espresso를 깨움.@Before fun setup() {
IdlingRegistry.getInstance().register(myIdlingResource)
}
@After fun tearDown() {
IdlingRegistry.getInstance().unregister(myIdlingResource)
}
Clock은 Compose 테스트에서 프레임 단위로 애니메이션, 재구성, LaunchedEffect 등을 구동하는 시계. UI의 layout, draw(일부)도 clock에 의해 동작.Clock이 움직이지 않으면(tick하지 않으면) 재구성이 일어나지 않고 애니메이션도 멈춤mainClock이 자동으로 시간을 진행시켜서, 대기 중인 재구성·애니메이션 등을 처리mainClock.autoAdvance = false이면 자동으로 프레임을 전진시키지 않음. 개발자가 직접 프레임 단위로 시간을 제어해야 함.mainClock.advanceTimeBy(ms) 통해 가상 시간을 ms만큼 전진시키고 그에 따른 프레임/애니메이션 진행을 실행advanceTimeBy() 이후 UI 렌더링 결과를 검증하려면 반드시 waitForIdle() 또는 runOnIdle을 호출해야 draw가 완료된 상태를 보장.waitForIdle()을 사용.mainClock은 Compose 프레임/애니메이션을 제어할 뿐, 코루틴 디스패처의 delay나 TestDispatcher의 가상 시간과는 별개. 즉 mainClock 조작은 코루틴 지연과는 상관 없음composeTestRule.mainClock.autoAdvance = false
// 애니메이션 시작 트리거
composeTestRule.onNodeWithText("애니메이션 시작").performClick()
composeTestRule.mainClock.advanceTimeBy(1000L) // 1초치 프레임 실행
composeTestRule.mainClock.autoAdvance = true
IdlingResource)를 기다림Dispatchers.Main의 코루틴 작업 특히 TestDispatcher에 스케줄된 작업은 자동으로 완료되지 않을 수 있음.mainClock의 auto advancement 여부에 따라 동작이 달라짐. 켜져 있는 경우 mainClock이 진행되면서 대기 중인 재구성, invalidation, 애니메이션을 처리. 꺼져 있는 경우 mainClock은 진행되지 않아 Compose UI는 정지(frozen)된 거처럼 보임. IdlingResource가 idle 상태가 될 때까지 계속 대기composeTestRule.onNodeWithText("버튼").performClick()
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText("완료").assertIsDisplayed()
runOnIdle 자체는 코루틴 디스패처의 대기(advance)까지 수행하지 않음. 블록 내에서 호출하면 가능.val text = composeTestRule.runOnIdle {
// UI thread에서 안전하게 상태를 읽음
viewModel.state.value
}
runCurrent() : 현재 가상 시간에 예약된(즉시 실행 가능한) 작업만 실행.advanceTimeBy(ms) : 가상 시간을 ms만큼 이동시키고 해당 시점에 도달한 작업 실행.viewModelScope.launch { ... }로 비동기 로직이 진행되고, UI는 idle이지만 코루틴 작업이 아직 대기 중일 때val testDispatcher = StandardTestDispatcher()
@Before fun setup() {
Dispatchers.setMain(testDispatcher)
}
@Test fun example() {
composeTestRule.onNodeWithText("로드").performClick()
// Compose가 idle인 시점에 코루틴 큐 전부 수행
composeTestRule.runOnIdle {
testDispatcher.scheduler.advanceUntilIdle()
}
composeTestRule.onNodeWithText("데이터").assertIsDisplayed()
}
waitUntil은 호스트 환경과 더 잘 통합되지만, 성능 측면에서는 덜 효율적.mainClock에서 auto advancement이면, 이 메서드는 조건이 만족될 때까지 clock을 진행시켜서 보류 중인 재구성, invalidation, 애니메이션을 처리.composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)composeTestRule.waitUntil(timeoutMillis = 5_000) {
composeTestRule.onAllNodesWithTag("item").fetchSemanticsNodes().isNotEmpty()
}
onNode...()로 노드를 찾아 assertion이나 interaction(클릭, 입력 등)을 수행. 이러한 API는 내부적으로 Compose의 동기화 메커니즘과 연결되어 있어서 적절한 시점까지 기다려주고 동작을 실행/검증함.waitForIdle()이나 runOnIdle { ... }호출 필요해짐assertIsDisplayed(), assertTextEquals() 같이 일반적인 UI 상호작용 및 검증composeTestRule.onNodeWithText("완료").assertIsDisplayed()
composeTestRule.onNodeWithTag("아이템1").performClick()
onNode(...).perform...() → waitForIdle() 또는 바로 onNode(...).assert...() composeTestRule.runOnIdle { testDispatcher.scheduler.advanceUntilIdle() }composeTestRule.mainClock.autoAdvance = false + mainClock.advanceTimeBy(...)waitUntil { condition }| 구분 | Compose 내부 동기화 | 코루틴/Dispatcher 동기화 | 조건 기반 동기화 | 외부 비동기 동기화 |
|---|---|---|---|---|
| 메커니즘 | waitForIdle(), runOnIdle{ }, Assertion/Interaction API (onNode().assert..., onNode().perform...), mainClock | advanceUntilIdle(),advanceTimeBy(ms), runCurrent() | waitUntil { condition } | Espresso IdlingResource |
| 동작 대상/설명 | Compose UI 트리 (recomposition, layout, draw), UI 스레드 실행, 내부 동기화, 프레임·애니메이션 시간 제어 | TestDispatcher의 코루틴 스케줄러 제어 (지연·예약 작업 처리) | 특정 조건이 충족될 때까지 상태 기반 대기 | 네트워크, WorkManager 등 Compose 외부의 비동기 작업 포함 |
| 사용 시점 | UI 변경만 확인, UI 상태 읽기, 애니메이션 시간 제어, 일반 상호작용/검증 | ViewModel 코루틴 로직 제어 필요할 때 | UI 노드 존재, 소멸, 특정 상태 변화 기다릴 때 | 외부 async 작업까지 테스트에서 동기화해야 할 때 |