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
}
}
contentDescription
가장 간단한 방법으로 contentDescription
을 사용할 수 있다.
@Before
fun setUp() {
composeTestRule.setContent {
Image(
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = "안드로이드 이미지",
)
}
}
@Test
fun `원하는_이미지를_표시하고_있는지_테스트`() {
composeTestRule
.onNodeWithContentDescription("안드로이드 이미지")
.assertIsDisplayed()
}
contentDescription
을 null
로 설정할 경우에는 테스트가 불가하다.contentDescription
을 가진 Image
가 여러 개 있다면 테스트가 어려워진다.contentDescription
도 번역된 문자열 리소스를 사용해야 하기 때문에 관리가 필요하다.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
유무와 상관없이 항상 사용할 수 있다.Semantics
Semantics
는 Text
컴포저블이 갖는 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
: 특정 이미지 리소스를 보여주는 것이 앱의 핵심 기능과 직결되어 반드시 특정 이미지가 사용되어야 함을 보장해야 할 때 사용할 수 있는 가장 확실한 방법이다.
잘 읽고 갑니다 ~ Gio 기오