본문 바로가기
개발/Compose

(Compose) Compose UI Test

by DinoDev 2025. 8. 5.
728x90
반응형

우리는 이제 멋진 화면을 만들고, 목록을 표시하고, 화면을 이동하는 방법까지 알게 되었습니다. 하지만 우리가 만든 코드가 앞으로도 계속 올바르게 동작할 것이라고 어떻게 보장할 수 있을까요? 디자인을 조금 바꾸거나 로직을 리팩터링 했을 때, 앱의 다른 기능이 고장 나지는 않았는지 어떻게 확인할까요?

매번 눈으로 모든 화면을 클릭해보는 것은 지치고 실수하기 쉽습니다. 이럴 때 필요한 것이 바로 자동화된 UI 테스트입니다. 오늘은 Jetpack Compose로 UI 테스트 코드를 작성하는 기본 방법을 알아보겠습니다.

UI 테스트, 왜 귀찮은 일을 추가하면서까지 해야 할까요?

어느 화창한 오후, PM에게서 간단한 요청을 받습니다.

"결제 화면의 '구매하기' 버튼 색깔을 좀 더 눈에 띠는 파란색으로 바꿔주세요. 그리고 문구도 '결제하기'로 수정해 주세요."

 

'이 정도면 5분도 안 걸리지!'

나는 자신만만하게 해당 Composable을 찾아 Modifier.background(Color.Blue)와 Text("결제하기")로 수정합니다. @Preview로 보니 완벽합니다. 코드를 커밋하고, 빌드하고, 배포까지 일사천리로 진행합니다.

그리고 다음 날 아침, 재앙이 시작됩니다.

고객센터에 불이 나고, 팀 메신저는 폭발합니다. 이유는 단 하나.

"결제하기 버튼을 눌러도 아무 반응이 없어요!"

나는 가슴이 철렁 내려앉은 당신은 코드를 파헤치기 시작합니다. 원인은 황당했습니다. 버튼 스타일을 수정하면서, 실수로 clickable Modifier를 감싸고 있던 다른 Modifier 안으로 옮겨버렸고, 이로 인해 클릭 이벤트가 제대로 전달되지 않았던 것입니다.

색깔과 문구만 바꿨을 뿐인데, 앱의 가장 중요한 기능인 '결제'가 막혀버렸습니다. 회사는 금전적 손실을 입었고, 사용자들은 분노했으며, 나는 동료들에게 사과하며 급하게 핫픽스를 배포해야 합니다. 그날 하루는 온전히 이 문제 해결에 소모됩니다.

만약 UI 테스트가 있었다면?

위 시나리오를 다시 돌려보겠습니다. '결제하기' 버튼을 수정한 후, 습관처럼 미리 작성해 둔 UI 테스트를 실행합니다.

테스트 시나리오: "결제 버튼을 클릭하면, 결제 확인 화면으로 이동해야 한다."

@Test
fun `결제_버튼을_클릭하면_확인화면으로_이동한다`() {
    // 1. Given: 결제 화면을 띄운다.
    // 2. When: '결제하기' 버튼을 찾아 클릭한다.
    // 3. Then: '결제 확인'이라는 텍스트가 화면에 나타나는지 검증한다.
}

만약 이런 간단한 UI 테스트가 있었다면 회사는 금전적 손실을 입지 않았을 것이고, 사용자들은 분노하지 않았을 것이고, 동료들에게 사과하러 다니는 나는 없었을 것이고 핫픽스를 하지도 않았을 것입니다.

UI 테스트는 '보험'이자 '안전망'입니다.

우리가 UI 테스트를 하는 이유는 단순히 버그를 찾기 위함만이 아닙니다.

  • 자신감을 얻기 위해: "이 코드를 고쳐도 다른 곳이 망가지지 않을 거야"라는 확신을 줍니다. 레거시 코드나 복잡한 로직을 두려움 없이 개선할 수 있는 자유를 줍니다.
  • 시간을 아끼기 위해: 매번 모든 기능을 손으로 테스트하는 끔찍한 반복 작업을 자동화된 로봇에게 맡기는 것입니다. 테스트 코드 작성에 1시간을 투자하면, 미래의 수십, 수백 시간의 수동 테스트와 버그 수정 시간을 아낄 수 있습니다.
  • 팀과 평화롭게 지내기 위해: 내가 수정한 코드가 동료의 코드를 망가뜨리는 일을 방지하고, 반대로 동료의 수정으로부터 내 코드를 보호합니다. 서로를 비난하는 대신, 테스트 코드가 조용히 문제를 알려줍니다.

UI 테스트는 귀찮은 추가 업무가 아닙니다. 프로 개발자로서 나의 결과물을 책임지고, 미래의 나에게 닥칠 재앙을 막아주는 가장 든든하고 현명한 안전장치인 것입니다.

테스트 환경 설정하기

Compose UI 테스트를 작성하려면 먼저 필요한 라이브러리를 추가해야 합니다.

app/build.gradle.kts 파일의 dependencies 블록에 다음을 추가하세요. (프로젝트를 처음 생성할 때 이미 추가되어 있을 수 있습니다.)

// UI 테스트를 위한 라이브러리 (androidTestImplementation)
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.8.3") // 버전을 확인하세요

// 디버그 빌드에서만 테스트 매니페스트를 사용하기 위함
debugImplementation("androidx.compose.ui:ui-test-manifest:1.8.3")

 

아마도 최신 프로젝트에서는 이렇게 직접 version을 명시하지 않고 bom 형태를 사용할 수 있습니다.

https://developer.android.com/develop/ui/compose/bom

 

재료명세서 사용  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 재료명세서 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose 재료명세서 (BOM)를 사용하면 BOM

developer.android.com

 

테스트 코드는 app/src/androidTest/java/ 디렉터리 안에 작성합니다.

Compose 테스트의 3가지 핵심 요소

Compose 테스트는 대부분 아래 3단계를 따릅니다.

  1. 찾기: 테스트하려는 UI 요소(노드)를 화면에서 찾습니다.
  2. 검증: 해당 노드가 원하는 상태(예: 화면에 보이는지, 텍스트가 맞는지)인지 확인합니다.
  3. 행동: 해당 노드에 특정 동작(예: 클릭, 텍스트 입력)을 수행합니다.

이 과정을 코드로 구현하는 방법을 알아봅시다.

테스트 룰(Rule)

모든 Compose 테스트의 시작점입니다. 테스트 환경을 설정하고 Composable을 렌더링 하는 역할을 합니다.

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class MyComposeTest {
    @get:Rule
    val composeTestRule = createComposeRule()
    // ... 테스트 코드
}

요소 찾기

composeTestRule을 통해 다양한 조건으로 노드를 찾을 수 있습니다.

  • onNodeWithText(): 특정 텍스트를 가진 노드를 찾습니다.
  • onNodeWithContentDescription(): Image나 IconButton의 contentDescription으로 찾습니다.
  • onNodeWithTag(): 테스트를 위한 전용 태그(testTag)로 노드를 찾는 방법입니다.
// Composable 코드에 testTag 추가하기
Text(
    text = "Hello",
    modifier = Modifier.testTag("GreetingText")
)

// 테스트 코드에서 testTag로 찾기
composeTestRule.onNodeWithTag("GreetingText")

검증과 행동

노드를 찾았다면, 이제 그 노드가 올바른지 확인하고 동작을 시킬 수 있습니다.

  • 주요 검증 함수 (Assertions)
    • assertIsDisplayed(): 화면에 보이는지 확인합니다.
    • assertExists(): 화면에 보이지 않더라도 노드 트리에 존재하는지 확인합니다.
    • assertIsEnabled() / assertIsNotEnabled(): 활성화/비활성화 상태인지 확인합니다.
    • assertTextEquals("..."): 텍스트 내용이 일치하는지 확인합니다.
  • 주요 행동 함수 (Actions)
    • performClick(): 클릭합니다.
    • performTextInput("..."): 텍스트를 입력합니다.
    • performScrollTo(): 특정 위치로 스크롤합니다.
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test

class CounterAppTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun counter_app_test() {
        // 1. Given (준비): 테스트할 Composable을 화면에 렌더링합니다.
        composeTestRule.setContent {
            CounterApp()
        }

        // 2. When (행동): '더하기' 버튼을 찾아 클릭합니다.
        composeTestRule.onNodeWithTag("AddButton")
            .performClick()
        composeTestRule.onNodeWithTag("AddButton")
            .performClick()

        // 3. Then (검증): 카운터 텍스트가 '2'로 변경되었는지 확인합니다.
        composeTestRule.onNodeWithTag("CounterText")
            .assertTextEquals("클릭 횟수: 2")
    }

    @Composable
    fun CounterApp() {
        var count by remember { mutableIntStateOf(0) }
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            Text(
                text = "클릭 횟수: $count",
                modifier = Modifier.testTag("CounterText")
            )
            Button(
                onClick = { count++ },
                modifier = Modifier.testTag("AddButton")
            ) {
                Text("더하기")
            }
        }
    }
}

이 테스트는 CounterApp을 실행하고, '더하기' 버튼을 두 번 누른 뒤, 텍스트가 "클릭 횟수: 2"로 올바르게 표시되는지 자동으로 검증해 줍니다.

 

마치며

이번 블로그를 통해서 Compose UI Test의 기본을 알아봤습니다. 앞으로 테스트 코드 작성을 습관화한다면 버그 걱정 없이 훨씬 더 안정적이고 품질 좋은 앱을 만들 수 있습니다.


블로그 글에 대해 궁금한 점이 있다면 아래 카카오톡 오픈채팅에 들어와서 질문해주세요

 

Android Kotlin Compose QnA

 

open.kakao.com

 

728x90
반응형

'개발 > Compose' 카테고리의 다른 글

(Compose) Lifecycle - Composition의 탄생부터 소멸까지  (2) 2025.08.08
(Compose) Stateful vs Stateless  (4) 2025.08.07
(Compose) LazyColumn  (3) 2025.08.05
(Compose) Modifier  (2) 2025.08.04
(Compose) 상태(state)  (2) 2025.08.04