들어가기 전에
테스트에 관련된 글을 쓰는게 정말 오랜만인 것 같네요. 저는 JUnit4, JUnit5, Kotest 를 사용한 단위 테스트, Robolectic 을 사용한 통합 테스트, Espresso 를 사용한 UI 테스트를 작성한 경험이 있습니다. Compose Test 는 최근에 NextStep 학습 테스트로 배우는 Compose 강의를 듣기 시작하면서 접하게 되었습니다.
UI 테스트는 여러 버전에서, 다양한 단말기가 문제 없이 의도한 대로 동작하는지 쉽게 테스트 할 수 있습니다. 에러를 쉽게 찾거나 앱의 퀄리티 향상에 도움이 되죠.
Key Concepts
Compose 코드 테스트의 핵심 개념은 다음과 같습니다.
- Semantics: Compose 테스트는 UI 의 각 부분에 각각 의미를 부여하는 semantics 와 상호작용합니다. semantics 는 UI 계층 구조와 함께 생성됩니다.
- Testing APIs: Compose 는 요소를 찾고(finders), 상태를 검증(Assertion)하며, 버튼 클릭 같은 액션(Actions)을 수행하는 Test API 를 제공합니다.
- Synchronization: 기본적으로 Compose 테스트는 UI와 자동으로 동기화되어, 어설션을 수행하거나 행동을 실행하기 전에 UI가 idle 상태가 될 때까지 기다립니다.UI가 idle 상태라는 것은 모든 pending 작업이나 애니메이션, recomposition 등이 완료되어 더 이상 변경 사항이 없는 상태를 의미합니다.
- Interoperability: 하이브리드 앱에서 Compose 와 View 기반 요소 모두와 상호작용 할 수 있습니다. 또한 다른 테스트 프레임워크와 통합해서 사용할 수 있습니다.
그 중에서 오늘은 Test API 에 대해 좀 더 알아보고 실제로 적용해보겠습니다.
Compose Testing API
앞에서 봤듯 Compose test 에서 UI 요소와 상호작용할 수 있는 방법은 세 가지가 있습니다.
- Finders : 하나 이상의 요소를 선택해 Assertion 을 만들거나 작업을 실행할 수 있습니다.
- Assertion: 요소가 존재하는지 혹은 어떤 속성을 가지는지 확인하는데 사용됩니다. 예를 들어 "환영합니다" 라는 텍스트가 존재하는지 확인할 수 있겠죠.
- Actions: user event 를 요소에 주입합니다. 클릭하거나 제스처를 취하는 등의 시뮬레이션이 가능합니다.
이 세 가지를 사용하면 "+" 라는 텍스트를 가진 버튼을 찾고(Finders) 버튼을 클릭한 뒤(Actions) count 가 1이 되는지 확인(Assertion)할 수 있겠죠? 그런 방식으로 사용할 수 있습니다. 공식 영상에서 보여준 계산기 예시처럼 1, +, 2, = 을 순서대로 클릭하고 결과가 3이 나오는지 확인할 수도 있습니다.
비밀번호 입력 창 만들기
실제로 어떻게 사용하는지 확인해봅시다. 간단한 요구사항을 만들고 기능이 잘 동작하는지 테스트해보겠습니다.
비밀번호 입력 창을 만든다.
- [ ] 비밀번호는 6자이다.
- [ ] 비밀번호는 숫자만 입력 가능하다. 문자 입력을 제한하지는 않는다.
- [ ] 입력한 비밀번호는 보이지 않아야한다.
TextField 로 비밀번호 입력 받기
기본 메테리얼 TextField 를 사용해 만들어보았습니다. 먼저 기본적으로 입력 가능하게 만들어 주었습니다.
@Composable
fun PasswordTextField(modifier: Modifier = Modifier) {
var password by remember { mutableStateOf("") }
TextField(
value = password,
label = { Text("비밀번호 입력") },
onValueChange = { value -> password = value},
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
)
}
비밀번호 가리기
요구사항을 만족할 수 있게 테스트를 먼저 작성해볼까요?
- [ ] 입력한 비밀번호는 보이지 않아야한다.
비밀번호는 입력해도 입력된 비밀번호가 보이지 않아야겠죠? 그럼 다음과 같이 테스트를 작성할 수 있습니다.
UI 테스트는 안드로이드 환경에서 동작하기 때문에 AndroidTest 패키지에 만들어주어야 합니다!
class PasswordTextFieldTest {
// JUnit Rule 로 컴포즈 테스트 환경 구성
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun 비밀번호를_입력해도_비밀번호가_보이지_않는다() {
val password = "123456"
// 테스트 중 렌더링 할 Compose UI
composeTestRule.setContent {
MaterialTheme {
PasswordTextField()
}
}
composeTestRule
.onNodeWithText("비밀번호 입력") // 텍스트로 비밀번호 UI 노드를 찾는다.
.performTextInput(password) // 비밀번호를 입력한다.
composeTestRule
.onNodeWithText(password)
.assertDoesNotExist() // 비밀번호가 보이는지 확인한다.
}
}
테스트를 실행해보면 바로... 실패! PasswordTextField 를 수정해주지 않았기 때문이죠.
visualTransformation 인자로 PasswordVisualTransformation 을 객체를 전달하고 다시 실행시켜보겠습니다.
@Composable
fun PasswordTextField(modifier: Modifier = Modifier) {
var password by remember { mutableStateOf("") }
TextField(
value = password,
label = { Text("비밀번호 입력") },
onValueChange = { value -> password = value },
visualTransformation = PasswordVisualTransformation(), // 추가
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
)
}
실행 결과.. 성공! 이런식으로 Composable 함수를 테스트 할 수 있습니다.
비밀번호 유효성 검사하기
남은 두 가지 요구사항에 대해서도 테스트를 해봅시다!
- [ ] 비밀번호는 6자이다.
- [ ] 비밀번호는 숫자만 입력 가능하다. 문자 입력을 제한하지는 않는다.
테스트는 다음 세 가지에 대해 작성하였습니다. 실제 코드라면 범위에 걸리는 것들에 대해 조금 더 많은 경우의 수를 테스트 해야겠지만 줄였습니다.
1. 6자리가 아닌 경우
2. 문자가 포함된 경우
3. 올바르게 입력된 경우
@Test
fun 비밀번호_길이가_6자리가_아니면_에러_메시지가_출력된다() {
val invalidPassword = "12345" // 5자리로, 6자리가 아님
composeTestRule.setContent {
MaterialTheme {
PasswordTextField()
}
}
// 비밀번호 입력란을 찾아 잘못된 비밀번호를 입력
composeTestRule
.onNodeWithText("비밀번호 입력")
.performTextInput(invalidPassword)
// 유효하지 않으므로 에러 메시지가 표시되어야 한다.
composeTestRule
.onNodeWithText("비밀번호는 6자리 숫자를 입력해야 합니다.")
.assertExists()
}
@Test
fun 비밀번호에_숫자외_문자가_포함되면_에러_메시지가_출력된다() {
val invalidPassword = "12345a" // 6자리이지만 숫자 외 문자가 포함됨
composeTestRule.setContent {
MaterialTheme {
PasswordTextField()
}
}
// 비밀번호 입력란을 찾아 잘못된 비밀번호를 입력
composeTestRule
.onNodeWithText("비밀번호 입력")
.performTextInput(invalidPassword)
// 에러 메시지가 나타나야 한다.
composeTestRule
.onNodeWithText("비밀번호는 6자리 숫자를 입력해야 합니다.")
.assertExists()
}
@Test
fun 올바른_비밀번호_입력시_에러_메시지가_보이지_않는다() {
val validPassword = "123456" // 6자리 숫자
composeTestRule.setContent {
MaterialTheme {
PasswordTextField()
}
}
// 올바른 비밀번호를 입력한다.
composeTestRule
.onNodeWithText("비밀번호 입력")
.performTextInput(validPassword)
// 에러 메시지가 없어야 한다.
composeTestRule
.onNodeWithText("비밀번호는 6자리 숫자를 입력해야 합니다.")
.assertDoesNotExist()
}
값이 입력되면 password 를 검증하고 유효하지 않은 경우에 ErrorMessage 를 보여줬습니다.
@Composable
fun PasswordTextField(modifier: Modifier = Modifier) {
var password by remember { mutableStateOf("") }
var passwordError by remember { mutableStateOf("") }
TextField(
value = password,
label = { Text("비밀번호 입력") },
onValueChange = { value ->
password = value
val isValid = password.length == 6 && password.all { it.isDigit() }
passwordError = if (!isValid) "비밀번호는 6자리 숫자를 입력해야 합니다." else ""
},
visualTransformation = PasswordVisualTransformation(), // 추가
isError = passwordError.isNotBlank(),
supportingText = { Text(passwordError) },
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
)
}
테스트가 모두 성공하는 것을 확인할 수 있습니다:)
UI 테스트의 WOW 포인트
굉장히 빠른 속도로 테스트가 실행되는 것을 확인할 수 있습니다. 기존 기능이 제대로 동작하는지, 새로운 기능에 문제 없는지 쉽고 빠르게 확인할 수 있어 매력적이었습니다. 끝!
출처
https://developer.android.com/develop/ui/compose/testing
https://developer.android.com/develop/ui/compose/testing/apis
'Jetpack Compose' 카테고리의 다른 글
[Jetpack Compose] 공식문서 읽기 Side-effects in Compose Part 1 (0) | 2025.01.26 |
---|---|
[Jetpack Compose] 컴포즈 Codelab 기초 따라하기 - 2 (0) | 2023.12.24 |
[Jetpack Compose] 컴포즈 Codelab 기초 따라하기 - 1 (2) | 2023.12.19 |
Compose 를 왜 사용해야하는가? (0) | 2023.12.09 |