Image
Testing Codes/Testing Android

[Android UI Test] ViewModel에 의존하는 Composable 테스트 방법 알아보기

ViewModel에 의존하는 Composable을 테스트할 때 문제점

Composable은 기본적으로 State Hoisting을 통해 ViewModel에 직접 의존하지 않도록 만들어야 하지만, 최상위에 있는 Screen  Composable은 상태값을 가지고 있는 ViewModel 객체에 의존해야 한다.

*만약 State Hoisting이 무엇인지 모른다면 다음 두 개의 글을 참고하도록 하자.

 

[Android Compose State] State Hoisting(상태 호이스팅) 패턴이란 무엇인가?

Compose의 State 선언형 UI 프레임워크인 Compose는 Stateless함이 가장 큰 장점이다. UI에 대한 UI상태의 상호 의존성을 끊을 수 있다면 UI의 재사용성이 생기고, UI에 대한 테스트 또한 가능해지기 때문이

kotlinworld.com

 

 

Jetpack Compose로 LINE 앱 Yahoo!검색 모듈 개발하기

들어가며 안녕하세요. LINE Android를 개발하고 있는 Service Client Dev 1 팀의 조세영입니다. 저희 팀은 최근 Yahoo!검색 모듈의 UI를 모두 Andro...

techblog.lycorp.co.jp

 

이런 경우, 우리는 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에 대한 참조를 가지지 않는다.

그림1. activity에 대한 참조가 없는 ComposeContentTestRule 인터페이스

 

이를 해결하기 위해 우리는 두가지 방법을 사용할 수 있다. 

  • 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()
    }
}

 

그러면 다음과 같이 테스트가 정상적으로 통과되는 것을 볼 수 있다.

그림2. 테스트 통과

 

정리

ViewModel에 의존하는 Composable을 테스트하기 위해서는 AndroidComposeTestRule을 통해 Activity를 가져와 ViewModel을 초기화해야 한다.

다만 이곳에서 다룬 ViewModel은 다른 객체에 의존성이 없는 ViewModel 이어서 초기화를 쉽게 할 수 있었다. ViewModel은 일반적으로 여러 객체에 대한 의존성을 갖기 때문에 이들 모두를 초기화하는 것은 쉽지 않은 일이다. 이런 문제를 해결하기 위해 Dagger-Hilt 같은 의존성 주입 프레임웍을 사용하면 손쉽게 ViewModel을 초기화하고 의존성을 주입할 수 있다.

 

 

 

반응형

 

이 글의 저작권은 '조세영의 Kotlin World' 에 있습니다. 글, 이미지 무단 재배포 및 변경을 금지합니다.

 

 

Kotlin, Android, Spring 사용자 오픈 카톡

오셔서 궁금한 점을 질문해보세요!
비밀번호 : kotlin22

open.kakao.com