ViewModel에 의존하는 Composable을 테스트할 때 문제점
Composable은 기본적으로 State Hoisting을 통해 ViewModel에 직접 의존하지 않도록 만들어야 하지만, 최상위에 있는 Screen Composable은 상태값을 가지고 있는 ViewModel 객체에 의존해야 한다.
*만약 State Hoisting이 무엇인지 모른다면 다음 두 개의 글을 참고하도록 하자.
이런 경우, 우리는 Screen Composable을 테스트하기 위해 ViewModel을 초기화하는 것이 필요하다. 이번 글에서는 이런 경우 ViewModel을 어떻게 초기화할 수 있는지 알아보도록 한다.
ViewModel에 의존하는 Composable 만들어 보기
ViewModel에 의존하는 간단한 ScreenComposable을 다음과 같이 만들어보자.
@Composable
fun SimpleTextScreen(textScreenViewModel: TextScreenViewModel) {
val textState by textScreenViewModel.textState.collectAsState()
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
text = textState
)
}
}
이런 모양의 ScreenComposable은 Activity에서 다음과 같이 사용된다.
class MainActivity : ComponentActivity() {
private val viewModel by viewModels<TextScreenViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AndroidUITestCourseTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
SimpleTextScreen(textScreenViewModel = viewModel)
}
}
}
}
}
*실제 애플리케이션에서는 코드를 조금 더 깔끔하게 작성한다. 여기서는 최상위에 있는 Composable을 표현하기 위해 이렇게 만들었을 뿐이다.
이런 경우 SimpleTextScreen을 테스트하기 위해서는 어떻게 해야할까? 지금부터 그 방법에 대해 알아보자.
ViewModel에 의존하는 Composable 테스트하기
ViewModel을 초기화 하기 위해서는 LifecycleOwner을 구현하는 객체가 필요하다. 이런 객체는 대표적으로 Activity나 Fragment가 있다. 이런 객체를 가져오기 위해서는 우리가 만드는 ComposeContentTestRule 객체로부터 해당 Rule이 가진 Activity를 가져와야 한다.
*Compose를 Fragment와 함께 사용하는 경우는 흔치 않지만, 함께 사용하는 경우도 있다.
이전 글에서 createComposeRule을 통해 만드는 ComposeContentTestRule 객체는 실제로 createAndroidComposeRule<ComponentActivity>()을 통해 만들어졌다는 것을 기억하자. 우리는 테스트 시 ComponentActivity를 실행할 것이므로, ComponentActivity를 LifecycleOwner로 만들어 ViewModel을 초기화하면 된다. 하지만, createComposeRule 함수를 사용해 ComposeContentTestRule 객체를 생성하면 Activity에 대한 참조를 가지지 않는다.
이를 해결하기 위해 우리는 두가지 방법을 사용할 수 있다.
- AndroidComposeTestRule 타입 캐스팅을 통해 Activity 가져오기
- createAndroidComposeRule<ComponentActivity>() 사용하기
지금부터 이 방법에 대해 자세히 알아보자.
타입 캐스팅을 통해 Activity 가져오기
우리가 createComposeRule을 통해 반환 받는 ComposeContentTestRule 객체는 구체적인 클래스 AndroidComposeTestRule를 인스턴스화해 만들어진 것이기 때문에 AndroidComposeTestRule로의 타입 캐스팅을 통해 Activity에 대한 참조를 얻을 수 있다.
class ComposeRuleTest {
@get:Rule
val composeRule: ComposeContentTestRule = createComposeRule()
@Test
fun assertTextDisplayed() = runBlocking<Unit> {
val activity = (composeRule as AndroidComposeTestRule<*, *>).activity
...
}
}
이렇게 얻어진 Activity를 사용해 TextScreenViewModel을 초기화 하면 유효한 ViewModel 객체를 얻을 수 있다.
val viewModel = ViewModelProvider(activity)[TextScreenViewModel::class.java]
createAndroidComposeRule<ComponentActivity>() 사용하기
두 번째 방법은 createComposeRule 함수 대신 createAndroidComposeRule 함수를 사용하는 것이다. createAndroidComposeRule 함수를 사용하면, AndroidComposeTestRule 객체가 반환되기 때문에 Activity에 대한 참조를 가진다. 따라서 다음과 같이 곧바로 ViewModel을 초기화할 수 있다.
class AndroidComposeRuleTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun assertAllEmojiTextDisplayed() = runBlocking<Unit> {
val viewModel = ViewModelProvider(composeRule.activity)[TextScreenViewModel::class.java]
...
}
}
ViewModel에 의존하는 Composable 테스트 실행해보기
이제 앞에서 초기화한 ViewModel을 활용해 테스트를 다음과 같이 완성해 보자.
class AndroidComposeRuleTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun assertAllEmojiTextDisplayed() = runBlocking<Unit> {
val viewModel = ViewModelProvider(composeRule.activity)[TextScreenViewModel::class.java]
composeRule.setContent {
SimpleTextScreen(textScreenViewModel = viewModel)
}
//When
viewModel.setText("Test String")
composeRule.awaitIdle()
// Then
composeRule.onNodeWithText("Test String").assertIsDisplayed()
}
}
그러면 다음과 같이 테스트가 정상적으로 통과되는 것을 볼 수 있다.
정리
ViewModel에 의존하는 Composable을 테스트하기 위해서는 AndroidComposeTestRule을 통해 Activity를 가져와 ViewModel을 초기화해야 한다.
다만 이곳에서 다룬 ViewModel은 다른 객체에 의존성이 없는 ViewModel 이어서 초기화를 쉽게 할 수 있었다. ViewModel은 일반적으로 여러 객체에 대한 의존성을 갖기 때문에 이들 모두를 초기화하는 것은 쉽지 않은 일이다. 이런 문제를 해결하기 위해 Dagger-Hilt 같은 의존성 주입 프레임웍을 사용하면 손쉽게 ViewModel을 초기화하고 의존성을 주입할 수 있다.