UI나 화면 테스트는 Compose 코드의 올바른 동작을 확인하는 데 사용되므로 개발 프로세스 초기에 오류를 포착하여 앱 품질을 개선할 수 있습니다.
Compose는 요소를 찾아 속성을 확인하고 사용자 작업을 실행하는 일련의 테스트 API를 제공합니다. 시간 조작과 같은 고급 기능도 포함되어 있습니다.
시맨틱
Compose의 UI 테스트는 시맨틱을 사용하여 UI 계층 구조와 상호작용합니다. 이름에서 알 수 있듯이 시맨틱은 UI 요소에 의미를 부여합니다. 이 컨텍스트에서 'UI 조각'(또는 요소)은 단일 컴포저블에서 전체 화면에 이르기까지의 어떤 것을 의미할 수 있습니다. 시맨틱 트리는 UI 계층 구조와 함께 생성되고 UI 계층 구조를 형성합니다.
그림 1. 일반적인 UI 계층 구조 및 시맨틱 트리
시맨틱 프레임워크는 주로 접근성에 사용되므로 테스트는 시맨틱에 의해 노출되는 UI 계층 구조 관련 정보를 활용합니다. 개발자는 노출할 내용과 양을 결정합니다.
그림 2. 아이콘 및 텍스트가 포함된 일반적인 버튼
예를 들어 아이콘과 텍스트 요소로 구성된 이 같은 버튼의 경우 기본 시맨틱 트리에는 '좋아요'라는 텍스트 라벨만 포함됩니다. Text
와 같은 일부 컴포저블이 일부 속성을 이미 시맨틱 트리에 노출하기 때문입니다. Modifier
를 사용하여 시맨틱 트리에 속성을 추가할 수 있습니다.
MyButton(
modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)
설정
이 섹션에서는 Compose 코드를 테스트할 수 있도록 모듈을 설정하는 방법을 설명합니다.
먼저, UI 테스트가 포함된 모듈의 build.gradle
파일에 다음 종속 항목을 추가합니다.
// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
이 모듈에는 ComposeTestRule
은 물론 AndroidComposeTestRule
이라는 Android용 구현이 포함되어 있습니다. 이 규칙을 통해 Compose 콘텐츠를 설정하거나 활동에 액세스할 수 있습니다. Compose의 일반적인 UI 테스트는 다음과 같습니다.
// file: app/src/androidTest/java/com/package/MyComposeTest.kt
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
// use createAndroidComposeRule<YourActivity>() if you need access to
// an activity
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = fakeUiState, /*...*/)
}
}
composeTestRule.onNodeWithText("Continue").performClick()
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
테스트 API
요소와 상호작용하는 주요 방법에는 세 가지가 있습니다.
- 파인더를 사용하면 요소(또는 시맨틱 트리의 노드)를 하나 이상 선택하여 어설션을 만들거나 작업을 실행할 수 있습니다.
- 어설션은 요소가 있는지 또는 특정 속성을 보유하는지 확인하는 데 사용됩니다.
- 작업은 클릭이나 기타 동작과 같은 시뮬레이션된 사용자 이벤트를 요소에 삽입합니다.
이러한 API의 일부는 시맨틱 트리에서 노드를 하나 이상 참조하는 SemanticsMatcher
를 허용합니다.
파인더
onNode
와 onAllNodes
를 사용하여 각각 노드를 하나나 여러 개 선택할 수 있지만 onNodeWithText
, onNodeWithContentDescription
등 가장 일반적인 검색에 편의 파인더를 사용할 수도 있습니다. Compose 테스트 요약본에서 전체 목록을 찾아볼 수 있습니다.
단일 노드 선택
composeTestRule.onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
composeTestRule
.onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")
여러 노드 선택
composeTestRule
.onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
composeTestRule
.onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")
병합되지 않은 트리 사용
일부 노드는 하위 요소의 시맨틱 정보를 병합합니다. 예를 들어 다음과 같이 텍스트 요소가 두 개 있는 버튼은 라벨을 병합합니다.
MyButton {
Text("Hello")
Text("World")
}
다음과 같이 테스트에서 printToLog()
를 사용하여 시맨틱 트리를 표시할 수 있습니다.
composeTestRule.onRoot().printToLog("TAG")
이 코드는 다음 출력을 생성합니다.
Node #1 at (...)px
|-Node #2 at (...)px
Role = 'Button'
Text = '[Hello, World]'
Actions = [OnClick, GetTextLayoutResult]
MergeDescendants = 'true'
병합되지 않은 트리가 될 요소의 노드를 일치시켜야 한다면 다음과 같이 useUnmergedTree
를 true
로 설정하면 됩니다.
composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")
이 코드는 다음 출력을 생성합니다.
Node #1 at (...)px
|-Node #2 at (...)px
OnClick = '...'
MergeDescendants = 'true'
|-Node #3 at (...)px
| Text = '[Hello]'
|-Node #5 at (83.0, 86.0, 191.0, 135.0)px
Text = '[World]'
useUnmergedTree
매개변수는 모든 파인더에서 사용할 수 있습니다. 예를 들어 여기서는 onNodeWithText
파인더에서 사용됩니다.
composeTestRule
.onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()
어설션
하나 이상의 매처가 있는 파인더가 반환한 SemanticsNodeInteraction
에서 assert()
를 호출하여 어설션을 확인합니다.
// Single matcher:
composeTestRule
.onNode(matcher)
.assert(hasText("Button")) // hasText is a SemanticsMatcher
// Multiple matchers can use and / or
composeTestRule
.onNode(matcher).assert(hasText("Button") or hasText("Button2"))
assertExists
, assertIsDisplayed
, assertTextEquals
등 가장 일반적인 어설션에 편의 함수를 사용할 수도 있습니다. Compose 테스트 요약본에서 전체 목록을 찾아볼 수 있습니다.
다음과 같이 노드 컬렉션에서 어설션을 확인하는 함수도 있습니다.
// Check number of matched nodes
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())
작업
노드에 작업을 삽입하려면 다음과 같이 perform…()
함수를 호출합니다.
composeTestRule.onNode(...).performClick()
다음은 몇 가지 작업의 예입니다.
performClick(),
performSemanticsAction(key),
performKeyPress(keyEvent),
performGesture { swipeLeft() }
Compose 테스트 요약본에서 전체 목록을 찾아볼 수 있습니다.
매처
이 섹션에서는 Compose 코드를 테스트하는 데 사용할 수 있는 몇 가지 매처에 관해 설명합니다.
계층적 매처
계층적 매처를 사용하면 시맨틱 트리에서 위 또는 아래로 이동하고 단순 일치를 실행할 수 있습니다.
fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher): SemanticsMatcher
다음은 이러한 매처가 사용되는 몇 가지 예입니다.
composeTestRule.onNode(hasParent(hasText("Button")))
.assertIsDisplayed()
선택기
테스트를 만드는 다른 방법은 일부 테스트를 더 읽기 쉽게 만들 수 있는 선택기를 사용하는 것입니다.
composeTestRule.onNode(hasTestTag("Players"))
.onChildren()
.filter(hasClickAction())
.assertCountEquals(4)
.onFirst()
.assert(hasText("John"))
Compose 테스트 요약본에서 전체 목록을 찾아볼 수 있습니다.
동기화
Compose 테스트는 기본적으로 UI와 동기화됩니다. ComposeTestRule
을 통해 어설션이나 작업을 호출하면 테스트가 미리 동기화되고 UI 트리가 유휴 상태가 될 때까지 기다립니다.
일반적으로 별도의 조치를 취할 필요가 없습니다. 하지만 몇 가지 극단적 사례에 관해 알아야 합니다.
테스트가 동기화되면 Compose 앱이 가상 클록을 사용하여 시간을 앞당깁니다. 즉 Compose 테스트가 실시간으로 실행되지 않으므로 최대한 빨리 통과할 수 있습니다.
하지만 테스트를 동기화하는 메서드를 사용하지 않는 경우 리컴포지션이 발생하지 않으며 UI가 일시중지된 것으로 표시됩니다.
@Test
fun counterTest() {
val myCounter = mutableStateOf(0) // State that can cause recompositions
var lastSeenValue = 0 // Used to track recompositions
composeTestRule.setContent {
Text(myCounter.value.toString())
lastSeenValue = myCounter.value
}
myCounter.value = 1 // The state changes, but there is no recomposition
// Fails because nothing triggered a recomposition
assertTrue(lastSeenValue == 1)
// Passes because the assertion triggers recomposition
composeTestRule.onNodeWithText("1").assertExists()
}
또한 이 요구사항은 앱의 나머지 부분이 아니라 Compose 계층구조에만 적용됩니다.
자동 동기화 사용 중지
assertExists()
와 같은 ComposeTestRule
을 통해 어설션이나 작업을 호출하면 테스트가 Compose UI와 동기화됩니다. 경우에 따라 이 동기화를 중지하고 클록을 직접 제어해야 할 수도 있습니다. 예를 들어 UI가 계속 사용 중일 때 애니메이션의 정확한 스크린샷을 캡처하는 시간을 제어할 수 있습니다. 자동 동기화를 사용 중지하려면 mainClock
의 autoAdvance
속성을 false
로 설정하세요.
composeTestRule.mainClock.autoAdvance = false
일반적으로 그런 다음에 직접 시간을 앞당깁니다. advanceTimeByFrame()
을 사용하여 정확히 한 프레임을 앞당기거나 advanceTimeBy()
를 사용하여 특정 기간만큼 앞당길 수 있습니다.
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
유휴 리소스
Compose는 모든 작업과 어설션이 유휴 상태에서 실행되도록 테스트와 UI를 동기화하여 필요에 따라 기다리거나 클록을 앞당길 수 있습니다. 하지만 결과가 UI 상태에 영향을 미치는 일부 비동기 작업은 테스트가 인식하지 못하는 동안 백그라운드에서 실행할 수 있습니다.
테스트에서 이 유휴 리소스를 만들고 등록하여 테스트 중인 앱이 사용 중인지 아니면 유휴 상태인지 파악할 때 고려할 수 있습니다. 예를 들어 Espresso 또는 Compose와 동기화되지 않는 백그라운드 작업을 실행하는 경우 추가 유휴 리소스를 등록해야 하지 않으면 조치를 취할 필요가 없습니다.
이 API는 Espresso의 유휴 리소스와 매우 유사하며 테스트 대상이 유휴 상태인지 아니면 사용 중인지 나타냅니다. Compose 테스트 규칙을 사용하여 IdlingResource
의 구현을 등록합니다.
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
수동 동기화
경우에 따라 Compose UI를 테스트의 다른 부분 또는 테스트 중인 앱과 동기화해야 합니다.
waitForIdle
은 Compose가 유휴 상태가 될 때까지 기다리지만 autoAdvance
속성에 종속됩니다.
composeTestRule.mainClock.autoAdvance = true // default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for Idling Resources to become idle
두 경우 모두 waitForIdle
은 대기 중인 그리기 및 레이아웃 단계도 기다립니다.
또한 advanceTimeUntil()
을 사용하여 특정 조건이 충족될 때까지 클록을 앞당길 수 있습니다.
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
지정된 조건은 클록의 영향을 받을 수 있는 상태를 확인해야 합니다(조건은 Compose의 상태만 확인함). Android의 측정 또는 그리기에 종속되는 모든 조건(즉 Compose 외부의 측정 또는 그리기)은 waitUntil()
과 같이 더 일반적인 개념을 사용해야 합니다.
composeTestRule.waitUntil(timeoutMs) { condition }
일반적인 패턴
이 섹션에서는 Compose 테스트에서 볼 수 있는 몇 가지 일반적인 접근 방식을 설명합니다.
개별적으로 테스트
ComposeTestRule
을 사용하면 전체 애플리케이션이나 단일 화면, 작은 요소 등 컴포저블을 표시하는 활동을 시작할 수 있습니다. 또한 컴포저블이 올바르게 캡슐화되어 있고 독립적으로 작동하는지 확인하는 것도 좋은 방법입니다. 그러면 더 쉽고 집중적인 UI 테스트가 가능합니다.
그렇다고 오직 단위 UI 테스트만 만들어야 한다는 의미는 아닙니다. UI의 더 큰 부분을 범위로 지정하는 UI 테스트도 매우 중요합니다.
자체 콘텐츠를 설정한 후 활동 및 리소스에 액세스
예를 들어 표시된 텍스트가 문자열 리소스와 일치하는지 어설션하기 위해 composeTestRule.setContent
를 사용하여 테스트 중인 콘텐츠를 설정해야 하고 활동 리소스에도 액세스해야 하는 경우가 종종 있습니다. 그러나 활동에서 이미 호출한 경우 createAndroidComposeRule()
로 만든 규칙에서 setContent
를 호출할 수 없습니다.
이를 위한 일반적인 패턴은 빈 활동(예: ComponentActivity
)을 사용하여 AndroidComposeTestRule
을 만드는 것입니다.
class MyComposeTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = exampleUiState, /*...*/)
}
}
val continueLabel = composeTestRule.activity.getString(R.string.next)
composeTestRule.onNodeWithText(continueLabel).performClick()
}
}
ComponentActivity
를 앱의 AndroidManifest.xml
파일에 추가해야 합니다. 이 종속 항목을 모듈에 추가하면 됩니다.
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
맞춤 시맨틱 속성
맞춤 시맨틱 속성을 만들어 테스트에 정보를 노출할 수 있습니다. 이렇게 하려면 새 SemanticsPropertyKey
를 정의하고 SemanticsPropertyReceiver
를 사용하여 사용 가능하도록 설정하세요.
// Creates a Semantics property of type boolean
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey
이제 semantics
수정자를 사용하여 이 속성을 사용할 수 있습니다.
val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
modifier = Modifier.semantics { pickedDate = datePickerValue }
)
테스트에서 SemanticsMatcher.expectValue
를 사용하여 속성 값을 어설션할 수 있습니다.
composeTestRule
.onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
.assertExists()
상태 복원 확인
활동이나 프로세스가 다시 만들어질 때 Compose 요소의 상태가 올바르게 복원되는지 확인해야 합니다. StateRestorationTester
클래스를 사용하여 활동 재생성에 의존하지 않고 이러한 검사를 실행할 수 있습니다.
이 클래스를 사용하면 컴포저블의 재생성을 시뮬레이션할 수 있습니다. rememberSaveable
의 구현을 확인하는 것이 특히 유용합니다.
class MyStateRestorationTests {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun onRecreation_stateIsRestored() {
val restorationTester = StateRestorationTester(composeTestRule)
restorationTester.setContent { MainScreen() }
// TODO: Run actions that modify the state
// Trigger a recreation
restorationTester.emulateSavedInstanceStateRestore()
// TODO: Verify that state has been correctly restored.
}
}
디버깅
테스트에서는 주로 시맨틱 트리를 확인하여 문제를 해결할 수 있습니다.
테스트 중 언제든지 composeTestRule.onRoot().printToLog()
를 호출하면 트리가 출력됩니다. 이 함수는 다음과 같은 로그를 출력합니다.
Node #1 at (...)px
|-Node #2 at (...)px
OnClick = '...'
MergeDescendants = 'true'
|-Node #3 at (...)px
| Text = 'Hi'
|-Node #5 at (83.0, 86.0, 191.0, 135.0)px
Text = 'There'
이러한 로그에는 버그를 추적하는 데 중요한 정보가 포함되어 있습니다.
Espresso와의 상호 운용성
하이브리드 앱에서는 Compose 컴포저블 내부의 뷰 계층 구조 및 뷰에서 (AndroidView
컴포저블을 통해) Compose 구성요소를 찾을 수 있습니다.
두 유형을 일치시키기 위해 특별한 단계가 필요하지는 않습니다. Espresso의 onView
를 통해 뷰를 일치시키고 ComposeTestRule
을 통해 Compose 요소를 일치시킵니다.
@Test
fun androidViewInteropTest() {
// Check the initial state of a TextView that depends on a Compose state:
Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
// Click on the Compose button that changes the state
composeTestRule.onNodeWithText("Click here").performClick()
// Check the new value
Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}
UiAutomator와의 상호 운용성
기본적으로 컴포저블은 편리한 설명자(표시된 텍스트, 콘텐츠 설명 등)를 통해서만 UiAutomator에 액세스할 수 있습니다. Modifier.testTag
를 사용하는 모든 컴포저블에 액세스하려면 특정 컴포저블 하위 트리에 testTagAsResourceId
시맨틱 속성을 사용 설정해야 합니다.
이 동작을 사용 설정하면 스크롤 가능한 컴포저블과 같이 다른 고유 핸들이 없는 컴포저블(예: LazyColumn
)에 유용합니다.
UiAutomator에서 Modifier.testTag
와 함께 중첩된 모든 컴포저블에 액세스할 수 있도록 컴포저블 계층 구조에서 한 번만 사용 설정할 수 있습니다.
Scaffold(
// Enables for all composables in the hierarchy.
modifier = Modifier.semantics {
testTagsAsResourceId = true
}
){
// Modifier.testTag is accessible from UiAutomator for composables nested here.
LazyColumn(
modifier = Modifier.testTag("myLazyColumn")
){
// content
}
}
Modifier.testTag(tag)
를 포함하는 모든 컴포저블은 resourceName
과 동일한 tag
로 By.res(resourceName)
을 사용하여 액세스할 수 있습니다.
val device = UiDevice.getInstance(getInstrumentation())
val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// some interaction with the lazyColumn
자세히 알아보기
자세한 내용은 Jetpack Compose 테스트 Codelab을 참고하세요.