사전 setting
Test를 작성하기 위한 의존성을 추가하자
androidTestImplemenation("androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplemenation("androidx.compose.ui:ui-test-manifest:$compose_version")
AndroidTest 패키지 내부에 Test파일을 생성하자
Test Rule를 생성하자.
@get:Rule
val rule = createComposerRule()
이 테스트룰은 우리가 테스트를할 content를 세팅하거나 테스트 내에서 앱과 상호작용할 수 있게 만들어 줍니다. 테스트 클래스는 항상 test rule이 정의되어있어야합니다.
테스트 코드 작성
@Test Annotation
테스트 도구 모음의 일부로 실행되어야 함을 나타내는 테스트 주석이 있는 메서드를 추가할 수 있습니다.
이 테스트에서 우리는 test Rule의 setContent 메서드를 통해서 우리의 컴포저블 스크린을 설정할 수 있습니다.
@get:Rule
val rule = createComposeRule()
@Test
fun SometingTest(){
rule.setContent { MyTestComposable() }
// Do something
// Check something
}
SemanticsNode
Composition은 앱의 UI를 설명하며 컴포저블을 실행하여 생성되고, UI를 설명하는 컴포저블로 구성된 트리구조입니다.
이 Composition의 옆에 Semantic Tree라는 병렬트리가 있습니다.
이 트리는 접근성 서비스(Accessibility Service) 및 테스트 프레임워크에서 이해할 수 있는 대체 방식으로 UI를 설명합니다.
접근성 서비스는 Semantic 트리를 사용하여 특정 요구 사항이 있는 사용자에게 앱을 설명합니다.
테스트 프레임워크는 이를 사용하여 앱과 상호작용하고 이에 대한 Assertion을 만듭니다.
Semantic Tree에는 컴포저블을 그리는 방법에 대한 정보가 포함되어 있지 않지만
컴포저블의 Semantic 의미에 대한 정보가 포함되어 있습니다.
이러한 정보를 Semantic 속성이라고 합니다. Text 컴포저블은 text라는 시맨틱 속성이 포함되어있습니다. text라는 시맨틱 속성이 Text 컴포저블의 의미 이기 때문입니다.
Icon은 contentDescription이라는 속성이 Icon의 의미를 설명합니다.
Compose 기반 라이브러리 위에 있는 컴포저블들은 시맨틱 속성이 이미 설정이 되어있고, 선택적으로 modifier를 사용하여 속성을 직접 설정하거나 재정의할 수 있습니다.
다시 테스트 이야기로 넘어와서 우리가 테스트를 진행하기 위해서는 해당 컴포저블 노드를 얻어야합니다. 이때 해당 컴포저블 노드와 상호작용하기 위해서 Semantic 속성을 사용합니다.
Testing APIs
컴포저블 요소들과 상호작용하는 API는 총 3개가 있습니다.
- Finders
- Semantics tree안에 Semantic속성과 매칭되는 한개 또는 여러개의 노드를 선택하고 액션을 수행하거나 assertions를 만듭니다.
- Assertions
- 해당 요소가 존재하거나 특정 속성을 가지고 있는지 검증하는데 사용됩니다.
- Actions
- 클릭이나 제스쳐 등 요소에 대한 사용자의 이벤트들을 노드에 주입합니다.
Finders
@Composable
fun Greeting(name: String){
Text(text = name)
}
우리는 테스트를 위한 컴포저블을 위와 같이 정의했습니다.
테스트를 진행하기 위해서는 해당 노드를 얻어야합니다.
이때 우리는 Semantic속성인 text를 통해서 해당 컴포저블 노드를 접근할 수 있습니다.
@Test
fun testGreetingText(){
rule.setContent{
Greeting("Android")
}
}
rule.onNode(hasText("Android")) // hasText는 SemanticsMatcher
rule.onNodeWithText("Android")
onNodeWithText(String)
- 인자로 받는 텍스트를 가지는 컴포저블과 매칭됩니다.
onNode(SemanticsMatcher)
- 해당하는 SemanticsMatcher에 매치되는 컴포저블과 매칭됩니다.
onNodeWithTag(TestTag)
기본적으로 컴포저블에 액세스하기 위해서는 text, contentDescription등을 통해서만 접근이 가능했지만, Modifier.testTag 시맨틱 속성을 사용설정해서 노드를 접할 수도 있습니다. 해당 테그가 설정된 컴포저블에 접근합니다.
SomethingComposable(
modifier = Modifier.testTag("hi")
)
Assertions
Finder로 맞는 한개 또는 여러개의 노드와 assert()를 호출하여 SemanticsNodeInteraction 검증을 시도합니다.
rule.onNode(matcher).assert(hasText("Android"))
rule.onNode(mather).assert(hasText("IOS") or hasText("Android"))
이러한 assertion은 미리 만들어져있는 것들이 있습니다.
- aasertExists
- assertIsDisplayed
- assertTextEquals
- more… 는 하단을 참고해주세요
Actions
perform()함수 호출을 통해서 해당 노드에 액션을 주입할 수 있습니다.
rule.onNode(matcher).performClick()
또한 다양한 perform함수를 지원합니다.
- performClick()
- performSemanticsAction(key)
- performKeyPress(keyEvent)
- performGesture { swipeLeft() }
- more… 하단 참고
동기화
Compose테스트는 기본적으로 UI와 동기화됩니다. Action이나 Assertion을 사용하면, 테스트는 미리 동기화되어, UI 트리가 idle이 될때까지 기다립니다.
테스트를 동기화하는 코드를 작성하지 않는다면, recomposition이 발생하지 않고, 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 // State는 바뀌었지만, recomposition이 진행되지 않아.
// 리컴포지션이 트리거 되지않아.
assertTrue(lastSeenValue == 1)
// 리컴포지션이 트리거된다.
**composeTestRule.onNodeWithText("1").assertExists()**
}
리컴포저션을 트리거위해서는 composeTestRule에서 제공하는 assertion이나 perform을 수행해야합니다.
그렇지 않은경우 state는 변경되었지만, recomposition이 트리거 되지 않습니다.
간단한 테스트 만들어보기
클릭을 했을 때 Text컴포저블의 text가 Android→ TestedAndroid이 변경되는지 테스트해보겠습니다.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "$name",
modifier = modifier,
)
}
// In Test.KT
class MyTextTest {
@get:Rule
val rule = createComposeRule()
@Test
fun testTextClick() {
var textText = mutableStateOf("Android")
rule.setContent {
Greeting(
name = textText.value,
modifier = Modifier.clickable {
textText.value = "Tested Android"
},
)
}
rule.onNode(hasText("Android")).performClick()
rule.onNodeWithText("Tested Android").assertExists()
}
}
마무리
지금까지 테스트 코드 작성에 대한 많은 걱정이 들었었습니다. 진입 장벽이 높은 느낌도 들고, 앱을 실행하면서 잘만 테스트를 할 수 있었는데 굳이 테스트 코드를 짜야할까? 라는 생각도 있었습니다.
위의 글에서는 아주아주 간단한 텍스트를 클릭해서 변경된 텍스트를 가지는 노드가 있는지 판단하였지만, 현재 개발하고 있는 프로젝트애서는 10개의 상태변경에 대한 제대로된 UI를 보여주고 있는지 혹은 bottom navigation의 route가 클릭을 했을 때 제대로 변경되고 있는지를 테스트를 해보았습니다.
막상 작성을 해보니, 잘 작성했다고 생각했던 코드에 문제가 있다는 점을 발견할 수 있었습니다. UI 테스트를 진행해보면서 제 코드에 대한 검증도 할 수 있어서 좋은 경험이였다고 생각합니다.
생각보다는 Ui 테스트 코드에 대한 장벽이 높지않은 것 같아서 차근차근하나씩 테스트 코드를 늘려가보시는 것을 추천드립니다. 이게 생각보다 재미있습니다.
번외
컴포저블의 color를 테스트하려면 어떻게 해야하는가?
하단 출처에 해당하는 유튜브 영상을 보면 후반부에 색상을 확인하는 방법이 없다고 이야기합니다.
그 이유는 우리가 노드에 접근하는 방법은 시맨틱 요소를 사용하여 접근하고, 그 시맨틱 속성은 컴포저블 자체의 의미이기도 합니다. ( Text는 text가 시맨틱 속성이였듯이 또는 Image는 이미지를 설명하는 contentDescription이 존재하듯이 )
컬러는 시각적 속성의 일부분일 뿐이지, 이 요소를 포함하는 컴포저블을 설명할 수는 없다고 생각이 들었습니다. 그렇기 때문에 color를 테스트할 수 없지 않을까라고 조심스럽게 생각을 적어봅니다.
또한 semantic이 접근성 서비스와 공유를 하다보니, 접근성을 통해 사용자에게 도움이 될수 있는 정보인가? 라고 생각을 해봤을때 그렇지 않다고 생각이 들었습니다.
( color를 테스트를 할 수 있는 방법은 있지만, 권장되는 테스트는 아닌 것 같습니다. 그래두 하단에 링크 첨부하겠습니다 .)
참고
https://www.youtube.com/watch?v=JyUJZvJ-OV8&t=1s
https://developer.android.com/jetpack/compose/testing
https://developer.android.com/jetpack/compose/semantics#properties
https://developer.android.com/jetpack/compose/testing-cheatsheet
https://stackoverflow.com/questions/70682864/android-jetpack-compose-how-to-test-background-color
'Compose' 카테고리의 다른 글
Jetpack Compose Theme 적용하기 (0) | 2023.06.07 |
---|---|
Compose와 함께 Motion Layout을 사용하여 애니메이션 구현하기 (0) | 2023.05.24 |
rememberUpdatedState (0) | 2023.02.15 |
SwipeToDismiss Jetpack Compose로 구현하기 (0) | 2023.02.02 |
Jetpack Compose GapBuffer (1) | 2022.10.27 |