Hyeyeon blog

[Android] Compose UI Test 본문

개발/Android

[Android] Compose UI Test

Hyeyeon.P 2025. 2. 26. 18:30
반응형

1. Compose의 Semantics 이란, 

Jetpack Compose의 UI 요소는 계층적인 시맨틱 트리(Semantics Tree)로 구성되며, 각 요소는 SemanticsNode로 표현됩니다. UI 테스트에서는 이 시맨틱 트리를 탐색하여 특정 요소(시맨틱 노드)를 찾아 테스트를 수행합니다.

아래와 같이, Column, Text, Button, Row 같은 시맨틱 요소들이 등록되어 트리를 구성하며, UI 테스트 시 이를 탐색합니다.

Root
 ├── Column
 │    ├── Text("Hello")
 │    ├── Button("Click!")
 │    └── Row
 │         ├── Text("Item 1")
 │         └── Text("Item 2")

🔎 시맨틱 트리 시각화

시맨틱 트리를 시각화하여 Logcat으로 확인할 수 있습니다. 

1) dependency 추가 

debugImplementation("androidx.compose.ui:ui-tooling:1.7.8")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.7.8")
debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.8")

2) Compose 테스트 코드 작성 

class ComposeTest {
    @get:Rule
    val composeTestRule = createComposeRule()
    
    val appContext = InstrumentationRegistry.getInstrumentation().targetContext
    
    @Test
    fun sementicTree() {
        composeTestRule.setContent {
            Theme {
                Column {
                    Text(text = "Hello")
                    Button(
                        onClick = {
                            Toast.makeText(appContext, "Show Toast", Toast.LENGTH_SHORT).show()
                        },
                    ) {
                        Text("Click!")
                    }

                    Row {
                        Text(text = "Item1")
                        Text(text = "Item2")
                    }
                }
            }
        }
    }
}

3) Logcat에서 시맨틱 트리  출력 

2. Compose 테스트 구성 요소 

1) Finder: UI 컴포넌트를 찾음

  • 예시 
composeTestRule.onNodeWithText("Hello")
  • 주요 함수
    • onNodeWithText("텍스트")
    • onNodeWithTag("태그")
    • onNodeWithContentDescription("설명")
    • onAllNodesWithText("텍스트")

    ※ 해당 텍스트를 가진 컴포넌트의 바로 윗 상위 노드 반환 
    ※ useUnmergedTree = true 시, 해당 컴포넌트 노드만 반환 

2) Assertion: 찾은 UI 컴포넌트가 기대하는 상태인지 확인  

  • 예시
composeTestRule
    .onNodeWithText("Hello")
    .assertIsDisplayed()
  • 주요 함수
    • assertIsDisplayed() – 화면에 보이는지 확인
    • assertTextEquals("텍스트") – 텍스트가 정확히 일치하는지
    • assertHasClickAction() – 클릭이 가능한지 (Modifier.clickable 설정 여부)  
    • assertIsEnabled() / assertIsNotEnabled() – 활성화 여부 

3) Action: 찾은 UI 컴포넌트를 대상으로 동작을 수행 

  • 예시
composeTestRule
    .onNodeWithText("Click!")
    .performClick()
  • 주요 함수
    • performClick() – 클릭
    • performTextInput("입력값") – 텍스트 입력
    • performScrollTo() – 스크롤
    • performTextClearance() – 입력된 텍스트 지우기

3. Jetpack Compose UI 테스트 환경 설정

build.gradle에 ui-test-junit4ui-test-manifest dependency를 추가합니다. 

dependencies {
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.0.5"
    debugImplementation "androidx.compose.ui:ui-test-manifest:1.0.5"
}

ComposeTestRule를 설정합니다. 
이는 JUnit 테스트에서 Compose 환경을 구성하고 테스트 함수에서 UI 조작과 검증을 수행합니다. 

class ExampleInstrumentedTest {
    @get:Rule
    val composeTestRule = createComposeRule()
    
    ...
}

4. Jetpack Compose UI 테스트 예제 

로그인 화면에서 이메일, 비밀번호를 모두 입력하면 로그인 버튼이 활성화되는 폼 검증 시나리오입니다. 

1) 컴포즈 코드 

이메일, 비밀번호 필드의 입력 값을 추적하기 위한 상태변수를 선언합니다.
이어, 버튼 활성화 여부에 사용될 이메일과 비밀번호가 모두 입력되었는지 확인하는 변수를 선언합니다. 

@Composable
fun LoginScreen() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    // 이메일과 비밀번호가 모두 입력되었는지 확인하는 변수
    val isFormValid = email.isNotEmpty() && password.isNotEmpty()

    Column(modifier=Modifier.padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        // 이메일 입력 필드
        TextField(
            value = email,
            onValueChange = { email = it },
            modifier=Modifier.border(1.dp, Color.Black)
                .testTag("Email")
        )
        Spacer(modifier = Modifier.height(8.dp))

        // 비밀번호 입력 필드
        TextField(
            value = password,
            onValueChange = { password = it },
            modifier=Modifier.border(1.dp, Color.Black)
                .testTag("Password")
        )
        Spacer(modifier = Modifier.height(8.dp))

        // 로그인 버튼
        Button(
            onClick = { /* 로그인 처리 로직 */ },
            enabled = isFormValid,
            modifier=Modifier.testTag("LoginButton")
        ) {
            Text(text = "Login")
        }
    }
}

2) 검증 코드

동작/상태 검증을 위한 테스트 코드를 작성합니다. 

@Test
fun composeUiTest() {
    composeTestRule.setContent {
        LoginScreen()
    }
    // 로그인 버튼 상태 확인 - 결과: 비활성화 (초기상태)
    composeTestRule.onNodeWithTag("LoginButton").assertIsNotEnabled()

    // 이메일 필드에 입력
    composeTestRule.onNodeWithTag("Email").performTextInput("test@gmail.com")

    // 로그인 버튼 상태 확인 - 결과: 비활성화 (이메일만 입력)
    composeTestRule.onNodeWithTag("LoginButton").assertIsNotEnabled()

    // 비밀번호 필드에 입력
    composeTestRule.onNodeWithTag("Password").performTextInput("12341234")

    // 로그인 버튼 상태 확인 - 결과: 활성화 (이메일, 비밀번호 입력)
    composeTestRule.onNodeWithTag("LoginButton").assertIsEnabled()
}

3) 결과

위와 같이 예상한 시나리오대로 동작하면 아래와 같이 passed 상태를 확인할 수 있습니다. 

만일 예상한 시나리오와 다르게 동작할 경우, 아래와 같이 failed 상태와 실패 로그가 발생합니다.

728x90
Comments