Jetpack Compose 이미지를 테스트하는 3가지 방법(contentDescription, testTag, Semantics)

Gio·2025년 9월 4일
2

Jetpack Compose에서는 UI 테스트를 위해 ComposeTestRule을 사용해볼 수 있다. onNodeWithText를 통해 원하는 컴포저블을 찾고 performClick을 통해 이벤트를 발생시킬 수 있다.

class Test {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun `Text_표시_테스트`() {
        composeTestRule.setContent {
            Text("Hello World!")
        }

        composeTestRule
            .onNodeWithText("Hello World!")
            .assertIsDisplayed()
    }

    @Test
    fun `onClick_이벤트_테스트`() {
        var clicked = false

        composeTestRule.setContent {
            Button(onClick = {
                clicked = true
            }) {
                Text("Hello World!")
            }
        }

        composeTestRule
            .onNodeWithText("Hello World!")
            .performClick()

        assert(clicked == true)
    }
}

이 글에서는 Image 컴포저블이 우리가 원하는 이미지를 잘 표시하고 있는지 체크할 수 있는 3가지 방법을 소개한다.

테스트 준비

class Test {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Before
    fun setUp() {
        composeTestRule.setContent {
            Image(
                painter = painterResource(R.drawable.ic_launcher_foreground),
                contentDescription = null,
            )
        }
    }

    @Test
    fun `원하는_이미지를_표시하고_있는지_테스트`() {
        // TODO
    }
}

방법 1: contentDescription

가장 간단한 방법으로 contentDescription을 사용할 수 있다.

@Before
fun setUp() {
    composeTestRule.setContent {
        Image(
            painter = painterResource(R.drawable.ic_launcher_foreground),
            contentDescription = "안드로이드 이미지",
        )
    }
}

@Test
fun `원하는_이미지를_표시하고_있는지_테스트`() {
    composeTestRule
        .onNodeWithContentDescription("안드로이드 이미지")
        .assertIsDisplayed()
}

장점

  • 구현이 간단하다.
  • 테스트를 작성함으로서 자연스레 접근성을 준수하도록 할 수 있다.

단점

  • contentDescriptionnull로 설정할 경우에는 테스트가 불가하다.
  • 동일한 contentDescription을 가진 Image가 여러 개 있다면 테스트가 어려워진다.
  • 앱이 여러 언어를 지원한다면 contentDescription도 번역된 문자열 리소스를 사용해야 하기 때문에 관리가 필요하다.

방법 2: testTag

테스트에서 UI 컴포넌트를 찾을 수 있도록 하는 태그를 추가하는 방법이다. 다음 API를 사용할 수 있다.

/**
 * Applies a tag to allow modified element to be found in tests.
 *
 * This is a convenience method for a [semantics] that sets [SemanticsPropertyReceiver.testTag].
 */
@Stable
fun Modifier.testTag(tag: String) = this then TestTagElement(tag)
@Before
fun setUp() {
    composeTestRule.setContent {
        Image(
            painter = painterResource(R.drawable.ic_launcher_foreground),
            contentDescription = "안드로이드 이미지",
            modifier = Modifier.testTag("R.drawable.ic_launcher_foreground"),
        )
    }
}

@Test
fun `원하는_이미지를_표시하고_있는지_테스트`() {
    composeTestRule
        .onNodeWithTag("R.drawable.ic_launcher_foreground")
        .assertIsDisplayed()
}

장점

  • contentDescription 유무와 상관없이 항상 사용할 수 있다.
  • 사용자에게 노출되지 않으므로 다국어 지원 이슈로부터 자유롭다.
  • 테스트용 식별자라는 명확한 목적을 갖고 있어 코드의 의도를 해치지 않는다.

단점

  • 프로덕션 코드에 테스트를 위한 코드가 추가된다.
  • 태그로 문자열을 사용하기 때문에 오타가 발생하도 인지하기 어렵다.

방법 3: 커스텀 Semantics

SemanticsText 컴포저블이 갖는 text 문자열같은 데이터와 달리 컴포넌트의 의미와 역할에 관한 추가 정보를 뜻한다. 예를 들어 카메라 아이콘은 단순한 이미지일 수 있지만, 의미론적인 의미로는 ‘사진 찍기’가 될 수 있다. 시멘틱을 통해 컴포저블에 대한 추가적 컨텍스트를 제공하여 접근성, 자동완성 기능, 테스트 등에 활용할 수 있다.

Image(
    painter = painterResource(R.drawable.camera),
    contentDescription = "사진 찍기",
    modifier = Modifier.clickable { ... }
            .semantics { role = Role.Button },
)

contentType, role, 등 접근성과 테스트를 위해 주로 사용되는 프로퍼티들은 이미 SemanticsProperties.kt 파일에 정의되어 있지만, Drawable 리소스 ID에 대한 속성은 존재하지 않는다.

따라서 커스텀 SemanticsProperty를 정의하여 사용할 수 있다.

val DrawableResId: SemanticsPropertyKey<Int> = SemanticsPropertyKey("drawableResId")
var SemanticsPropertyReceiver.drawableResId: Int by DrawableResId
  • SemanticsPropertyKey 객체는 고유한 키 역할을 하는 싱글톤 객체로 취급되기에 파스칼 케이스를 사용한다.
    • SemanticsProperties.kt 내부를 보면 다른 SemanticsPropertyKey 객체들도 그렇게 정의된 것을 확인할 수 있다.
      object SemanticsProperties {
          val ContentDescription = AccessibilityKey<List<String>>( ... )
          val StateDescription = AccessibilityKey<String>(...)
      }

이제 Image에 커스텀 시맨틱 속성을 적용하고, 이를 기반으로 테스트할 수 있다.

@Before
fun setUp() {
    composeTestRule.setContent {
        Image(
            painter = painterResource(R.drawable.ic_launcher_foreground),
            contentDescription = "안드로이드 이미지",
            modifier = Modifier.semantics { drawableResId = R.drawable.ic_launcher_foreground },
        )
    }
}

@Test
fun `원하는_이미지를_표시하고_있는지_테스트`() {
    composeTestRule
        .onNode(hasDrawableResId(R.drawable.ic_launcher_foreground))
        .assertIsDisplayed()
}

private fun hasDrawableResId(id: Int): SemanticsMatcher = SemanticsMatcher.expectValue(DrawableResId, id)

장점

  • 실제 Drawable 리소스 ID를 직접 비교하므로 오타 발생을 줄일 수 있다.
  • 테스트의 의도가 "특정 이미지 리소스를 사용하는가?"로 더욱 명확해진다.

단점

  • 다른 방법에 비해 초기 설정(커스텀 속성 정의, SemanticsMatcher 정의)이 복잡하다.
  • painterResource를 사용하지 않는 경우(예: BitmapPainter, AsyncImage 등)에는 이 방법을 적용할 수 없다.

결론

이렇게 Jetpack Compose에서 이미지를 테스트하는 방법에 대해 알아보았다. 물론 정답은 없다. 상황에 맞는 방식을 적용할 수 있다. 각 방식을 간단히 정리하고 글을 마무리한다.

  • contentDescription: 접근성이 중요하고 간단한 확인만 필요할 때 좋은 선택지이다.
  • testTag: contentDescription을 사용할 수 없거나, 코드의 의도를 명확히 하고 싶을 때 사용할 수 있는 선택지이다.
  • 커스텀 Semantics: 특정 이미지 리소스를 보여주는 것이 앱의 핵심 기능과 직결되어 반드시 특정 이미지가 사용되어야 함을 보장해야 할 때 사용할 수 있는 가장 확실한 방법이다.
profile
틀린 부분을 지적받기 위해 업로드합니다.

2개의 댓글

comment-user-thumbnail
2025년 9월 5일

잘 읽고 갑니다 ~ Gio 기오

1개의 답글