개발자로서의 삶을 더욱 윤택하게 만드려면 스스로 테스트 코드를 작성하고 자신의 코드나 다른 사람의 코드를 디버깅하는 데 익숙해야만 한다.
유닛 테스트: 비즈니스 로직이 예상대로 동작함을 확인해야만 한다.
통합 테스트: 앱의 모든 구성 요소가 적절히 통합되어 있는지 확인한다. ex) 앱이 하는 일에 따라 원격 서비스에 접근, DB와 연동, 디바이스에 파일을 읽고 쓰는 동작을 포함할 수 있다.
UI 테스트: UI가 정확히 구현되었는지 테스트한다. 지원하는 모든 화면 크기에 대해 모든 UI 요소가 잘 나타나는지, 항상 적절한 값을 보여주는지, 버튼을 클릭하거나 슬라이더를 이동하는 등의 상호작용이 의도된 함수를 호출시키는지? 앱의 모든 영역이 접근성을 가지는 지 확인해야 한다.
테스트 피라미드: Unit Test - 통합 테스트 - UI 테스트로 이어지는 구조
유닛 테스트 구현
Unit은 작고 고립된 코드 조각으로, 프로그래밍 언어에 따라 일반적으로 function, method, sub routine, property가 이에 해당한다.
Test class는 하나 이상의 테스트를 포함한다.
테스트는 잘 정의된 상황이나 조건 또는 기준이 포함되어 있는지를 확인한다.
테스트는 격리되어야 한다.
테스트는 이전 테스트에 의존해서는 안 된다.
**단언문(assertion)**은 예상되는 행위를 나타낸다. 단언문을 충족시키지 못하면 테스트는 실패한다.
Composable 함수 테스트
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
@Composable funSimpleButtonDemo() { val a = stringResource(id = R.string.a) val b = stringResource(id = R.string.b) var text by remember { mutableStateOf(a) } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Button(onClick = { text = if (text == a) b else a }) { Text(text = text) } } }
@get:Rule var name = TestName() // 테스트 메서드 내부에서 현재 테스트 이름을 제공할 수 있게 해준다.
/* createComposeRule: ComposeContentTestRule 구현체, AndroidComposeTestRule<ComponentActivity> createAndroidComposeRule: ComponentActivity 이외의 액티비티 클래스용 AndroidComposeTestRule을 생성할 수 있게 해준다. */ @get:Rule val rule: ComposeContentTestRule = createComposeRule()
@Before funsetup() { // 테스트할 Composable 함수 로딩 // 테스트마다 정확히 한 번만 호출되어야 한다. rule.setContent { SimpleButtonDemo() } }
@Test funtestInitialLetterIsA() { // onNodeWithText: finder라 불린다. 시맨틱 노드에서 동작한다. // 특정 컴포저블이 기대한 대로 나타나거나 동작하는지 테스트하려면 컴포즈 계층 구조의 모든 자식 사이에서 해당 컴포저블을 찾아내야 한다. // 여기서 시맨틱 트리가 동작한다. -> UI 계층 구조와 동시에 생성되며 Rule, Text, Action과 같은 속성을 사용해 계층 구조를 설명한다. rule.onNodeWithText("A").assertExists() }
```kotlin // 1. 사전 정의 val BackgroundColorKey = SemanticsPropertyKey<Color>("BackgroundColor") var SemanticsPropertyReceiver.backgroundColor by BackgroundColorKey
// 2. Composable 작성 @Composable fun BoxButtonDemo() { var color by remember { mutableStateOf(COLOR1) } Box( modifier = Modifier .fillMaxSize() .testTag(TAG1) .semantics { backgroundColor = color } // 프로퍼티 설정 .background(color = color), contentAlignment = Alignment.Center ) { Button(onClick = { color = if (color == COLOR1) COLOR2 else COLOR1 }) { Text(text = stringResource(id = R.string.toggle)) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 3. 테스트 코드에서 접근 @RunWith(AndroidJUnit4::class) classBoxButtonDemoTest{
@get:Rule val rule = createComposeRule()
@Test fun 텍스트박스의_배경색이_COLOR1인_노드가_존재한다() { rule.setContent { BoxButtonDemo() } // SemanticsMatcher의 expectValue()와 해당 프로퍼티 key 값을 이용해 값이 동일한지 확인한다. rule.onNode(SemanticsMatcher.expectValue(BackgroundColorKey, COLOR1)).assertExists() // equals가 아닌 일치하는 Node가 있는지 존재 유무를 따짐 } }
컴포즈 앱 디버깅
직접 해보는게 제일 좋을 것 같다.
추가적인 팁
커스텀 Modifier를 통한 현재 기본값 출력
inspectorInfo parameter
디버그 인스펙터 정보 확인하기
1 2 3 4 5 6 7 8 9 10 11 12
isDebugInspectorInfoEnabled = true// InspectableValue.kt 전역변수 설정
Modifier.semantics { backgroundColor = color }.also { (it as CombinedModifier).run { valinner = this.javaClass.getDeclaredField("inner") inner.isAccessible = true val value = inner.get(this) as InspectorValueInfo value.inspectableElements.forEach { ve: ValueElement -> Log.i("ValueElement", "value element: $ve") // 원하는 대로 값을 뽑아 사용 가능 } } }
그래서 나온 결과는 아래와 같았다.
1 2
I/ValueElement: value element: ValueElement(name=mergeDescendants, value=false) I/ValueElement: value element: ValueElement(name=properties, value=Function1<androidx.compose.ui.semantics.SemanticsPropertyReceiver, kotlin.Unit>)