요즘 채용공고를 보면 자격요건 또는 우대요건에 Unit Test, UI Test와 같은 요구사항이 자주 등장합니다.
제가 속해있던 회사들에선 주로 Unit Test를 필수로 작성하였습니다. 그리고 Jenkins와 같은 툴을 통해 자동화된 테스트를 수행하여 제품의 안정성과 품질을 유지 또는 향상시키기 위해 노력했습니다.
그러나 UI Test의 경우 리소스와 같은 현실적인 요건, 그리고 QA를 통한 검증 등을 핑계로 작성되지 않는 경우가 잦았습니다.
지난 과거를 반성도 해볼 겸, Compose에서 UI Test를 작성하는 방법에 대해 리뷰해보고자 합니다.
https://developer.android.com/develop/ui/compose/testing/testing-cheatsheet
구글의 공식 문서에 다음과 같은 요약본 (Cheat sheet)를 제공합니다.
이것은 Compose로 UI Test를 작성할 때 필요한 구성요소들에 대한 요약본을 제공합니다.
이 포스팅은 이것을 리뷰해보기로 하겠습니다.
보시다시피 다양한 구성요소들이 있습니다.
기존 안드로이드 뷰를 사용하며 UI Test를 작성해본 경험이 있으시다면 익숙한 부분이 있을 것이라 생각됩니다.
그럼 하나씩 뜯어보도록 하죠.
Finders
Finders는 이름에서 유추할 수 있듯이, 무언가를 찾기 위한 API를 나타냅니다.
onNode와 같은 네이밍에서 볼 수 있듯, Compose의 어떤 노드를 찾는데 사용되겠죠.
UI Test를 작성한다고 가정해보면, 화면에 나타나는 UI요소가 무엇이 있는지를 찾아야 합니다.
이것은 Compose에서 노드를 찾는 것으로 해결할 수 있습니다.
예를들어, "ABCD"라는 텍스트가 있는 노드를 찾고 싶다면, onNodeWithText("ABCD") 와 같이 코드를 작성하게 되겠죠.
우리는 Finders에 속한 API를 통해 검증하고자 하는 Compose의 노드를 찾는다로 이해하면 되겠습니다.
Matchers
Matchers는 어떤 조건에 부합하는 Compose의 노드를 찾아내는 역할을 합니다.
예를들어 어떤 상태에 의해 Compose 트리가 구성이 되었을 때, "ABCD" 텍스트를 가진 노드를 찾고싶다고 해보죠.
그럼 우리는 Node를 찾기 위해 Finders API를 활용할테고, 그 API에서 "ABCD" 텍스트에 해당하는 노드를 찾기 위해 Matcher를 전달해야 합니다.
즉, onNode(hasText("ABCD")) 와 같은 코드로 표현됩니다.
우리는 Finders로 어떤 Node를 찾을 때, 찾고자 하는 Node의 조건을 알려주기 위해 Matchers API를 사용한다로 이해하면 되겠습니다.
Assertions
Assertions는 테스트하고자 하는 결과를 검증하는 API의 묶음입니다.
Unit Test를 작성해보았다면 별도의 설명없이도 이해가 가능할 것입니다.
어떤 UI를 테스트한다면,
"UI가 어떤 상태가 되었을 때, 어떤 텍스트는 ABCD이다." 혹은 "UI가 X 상태가 되었을 때, 어떤 버튼은 Y이다." 와 같은 시나리오가 될 것입니다.
즉, Finders와 Matchers로 테스트 대상이 되는 Node를 찾았다면 그 결과를 검증하는 것이 끝이 될텐데, 그 때 사용하는 API의 묶음이 Assertions가 되는 것입니다.
예를들어 현재 화면에 표시되는 노드들을 찾고, 그러한 노드들이 존재하는지를 검증하는 테스트 코드를 작성한다면 다음과 같습니다.
onNode(isDisplayed()).assertExists()
Actions
Actions에 대해 설명하기 전에 우리의 UI를 한번 상상해봅시다.
우리의 UI는 사용자에게 어떤 텍스트, 이미지, 버튼 등을 제공합니다. 그리고 사용자의 행위에 따라 어떤 로직들을 수행합니다.
그렇다면, 테스트 시나리오는 다음과 같이 될 수도 있습니다.
"UI에 텍스트, 버튼이 제공되었을 때, 사용자가 버튼을 누른다면, UI의 다음 상태는 Y이다."
즉, 우리는 테스트 코드에 "사용자의 행위"를 나타낼 수 있어야 합니다.
아마 감이 오셨을텐데, Actions는 이것을 위한 API의 묶음입니다.
예를들어 로그인 버튼을 눌렀을 때 로직이 올바르게 작동하면 환영 메시지를 보여주는 시나리오에 대한 테스트 코드는 다음과 같습니다.
// 1. Login 버튼에 대한 노드를 찾아 클릭합니다.
onNodeWithText("Login").performClick()
// 2. Welcome 메시지가 표시되는지 확인합니다.
onNodeWithText("Welcome").assertExists()
ComposeTestRule, AndroidComposeTestRule
우리가 어떤 코드를 작성하고 동작시키기 위해선 환경이 굉장히 중요합니다.
테스트코드는 어떤 테스트를 작성하느냐에 따라서 적절한 환경을 제공해주어야 합니다.
여기서 환경을 제공해주기 위한 것으로 TestRule이란 것을 사용하며, Compose를 위한 것으로 "ComposeTestRule"을 사용합니다.
ComposeTestRule은 테스트코드에서 Compose를 위한 환경을 설정하고 이러한 환경 아래에서 Compose의 상태를 관리하거나, 다양한 API와 상호작용하거나, 애니메이션의 동작같은 것을 할 수 있도록 제공합니다.
이 내용을 통합하면, 테스트 코드는 다음과 같이 작성됩니다.
class MyComposeTest {
//
@get:Rule
val composeTestRule = createComposeRule()
// 또는 createAndroidComposeRule<MyActivity>()
@Test
fun myTest() {
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = uiState, /*...*/)
}
}
composeTestRule.onNodeWithText("Login").performClick()
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
특히, AndroidComposeTestRule은 특정 Activity에 대한 Compose 테스트를 해야하는 경우에 사용할 수 있습니다.
Debug
Debug는 테스트 코드에서 디버깅을 위한 용도로 사용됩니다.
테스트 코드를 작성하다보면 종종 다양한 문제에 직면합니다.
테스트코드를 잘못 작성했다던지, UI를 잘못 구현했다던지 등이 있겠죠.
이럴때 문제가 뭔지 진단해야 어떻게 고쳐야할 지 알 수 있겠죠.
이러한 방법을 제공하기 위한 도구 중 하나로 디버깅용 API의 묶음을 제공합니다.
그리고 이것은 다음과 같은 코드를 작성하고 결과를 얻어낼 수 있습니다.
composeTestRule.onRoot().printToLog()
// composeTestRule.onNodeWithText("Click").printToLog("ButtonDebug")
// 예시 결과
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'
지금까지 Compose를 위한 UI Test 작성 API들의 묶음을 전체적으로 리뷰해보았습니다.
API들의 역할, 그리고 어떻게 연결되는지를 이해한다면 테스트 작성 시 큰 어려움 없이 작성할 수 있으리라 생각합니다.
다음 포스팅은 실제 예시 코드들과 함께 테스트 코드를 작성해보겠습니다.
'개발 > Android' 카테고리의 다른 글
[Android] APK 파헤치기 (0) | 2025.01.05 |
---|---|
[Android] Circuit + Compose로 Pokedex를 구현해보자. (feat. MVI) (2) | 2024.12.01 |
[Android/안드로이드] Jetpack Compose - Stability와 Recomposition 그리고 최적화 (0) | 2024.06.27 |
[Android/안드로이드] Jetpack Compose - 폰트 크기 고정하기 (0) | 2024.06.17 |
Android - EncryptedSharedPreferences 로 데이터 암호화하기 (0) | 2023.11.14 |