안드로이드 시스템·Compose 용어 사전
테스트semantics · createAndroidComposeRule · onNodeWithText

Compose UI 테스트

semantics 트리를 기준으로 컴포저블을 찾고 조작하는 테스트. id가 필요 없다.

semantics 트리를 기준으로 컴포저블을 찾고 조작한다.

@get:Rule val rule = createAndroidComposeRule<MainActivity>()

@Test fun 검색어를_입력하면_결과가_보인다() {
    rule.onNodeWithText("검색").performTextInput("코틀린")
    rule.onNodeWithTag("resultList").assertIsDisplayed()
    rule.onNodeWithText("결과 없음").assertDoesNotExist()
}

semantics가 무엇인가

Compose 는 화면을 그리면서 '의미 트리' 를 함께 만든다

  • 이 노드는 버튼이다 · 이 텍스트는 "검색" 이다 · 이 요소는 비활성이다

이 트리는 두 소비자가 쓴다

  • 접근성 서비스 (TalkBack)

  • 테스트 프레임워크

  • 테스트를 잘 쓰면 접근성도 함께 좋아진다

무엇으로 찾나

onNodeWithText("저장")                     // 사용자가 보는 것 — 가장 권장된다
onNodeWithContentDescription("뒤로")       // 아이콘
onNodeWithTag("resultList")                // 테스트 전용 태그

// 태그는 이렇게 붙인다
Modifier.testTag("resultList")

텍스트로 찾는 것이 원칙이다 — 사용자 관점에 가깝다 태그는 텍스트가 없거나 중복될 때만 쓴다 (구현 세부에 결합되므로)

자동 동기화가 핵심 편의

테스트는 앱이 '유휴 상태' 가 될 때까지 자동으로 기다린다

  • 리컴포지션 · 애니메이션 · Compose 코루틴이 끝날 때까지

  • Thread.sleep 이 필요 없다

예외: 자체 스레드/외부 비동기는 모른다

  • rule.mainClock.autoAdvance = false 로 수동 제어하거나
  • waitUntil { ... } 로 조건을 건다

컴포저블만 단독으로 테스트

@get:Rule val rule = createComposeRule()      // Activity 없이

@Test fun 로딩이면_스피너가_보인다() {
    rule.setContent { HomeScreen(state = HomeUiState(isLoading = true), onRefresh = {}) }
    rule.onNodeWithTag("spinner").assertIsDisplayed()
}

상태 호이스팅이 돼 있으면 이런 테스트가 가능하다

  • 상태를 주입해 모든 경우를 검증한다
  • 테스트하기 쉬운 설계가 곧 좋은 설계인 이유

Espresso와의 차이

  • Espresso — View 시스템. id 로 찾는다 (onView(withId(R.id.button)))
    • Compose 화면은 하나의 AndroidComposeView 로만 보인다

혼용 화면이면 두 API 를 함께 쓴다

  • createAndroidComposeRule 이 Espresso 와 동기화를 공유한다

면접 함정

  • Thread.sleep으로 기다린다 → 자동 동기화가 있다. 느려지고 불안정해진다.
  • "UI 테스트를 많이 쓸수록 좋다" → 느리고 깨지기 쉽다. 피라미드의 꼭대기다.

커스텀 semantics로 테스트 가능성을 높인다

val RatingKey = SemanticsPropertyKey<Int>("Rating")
var SemanticsPropertyReceiver.rating by RatingKey

Modifier.semantics { rating = 4 }

// 테스트
rule.onNode(SemanticsMatcher.expectValue(RatingKey, 4)).assertExists()

리스트에서 스크롤해 찾기

rule.onNodeWithTag("list")
    .performScrollToNode(hasText("100번째 항목"))
    .assertExists()

// 자식 개수 확인
rule.onAllNodesWithTag("row").assertCountEquals(20)

시계를 직접 돌린다

rule.mainClock.autoAdvance = false
rule.onNodeWithText("시작").performClick()
rule.mainClock.advanceTimeBy(500)          // 애니메이션 중간 상태를 검증한다
rule.onNodeWithTag("progress").assertExists()

디버깅

rule.onRoot().printToLog("TREE")     // semantics 트리 전체를 로그로 본다

"노드를 못 찾는다" 는 대부분

  • contentDescription 이 없다
  • mergeDescendants 로 자식이 합쳐졌다 (useUnmergedTree = true 로 본다)
  • 아직 컴포지션에 없다 (스크롤 밖)

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 테스트 — 단위·Compose·Flow·Hilt