[codelab 5.1 -8 9 10]AndroidX 테스트로 ViewModel(LiveData 관찰되는지) 테스트
Android/Android Test

[codelab 5.1 -8 9 10]AndroidX 테스트로 ViewModel(LiveData 관찰되는지) 테스트

728x90
반응형

CodeLab 05.1 Testing Basics

 

목차

  1. (Task 8) [ViewModel]테스트
  2. (Task 9) [LiveData] 테스트
  3. (Task 10) multiple [ViewModel]테스트

이번코드랩에서는 대부분의 앱에서 공통적으로 사용되는 두 가지 Android 클래스(ViewModel & LiveData)에 대한 테스트를 작성하는 방법을 배웁니다. 

 

 


Task 8 : AndroidX 테스트로 ViewModel 테스트 설정

Setting up a ViewModel Test with AndroidX Test


ViewModel에 모든 로직이 있고 Repository에 의존하지 않는 테스트를 수행하려 합니다. 

Repository 코드에는 테스트를 더 복잡하게하는 '비동기 코드', '데이터베이스' 및 '네트워크 호출' 코드가 존재합니다.  

이번 챕터에서는 일단 지금은 이를 피하고 Repository에서 직접 테스트하지 않는 ViewModel 기능에 대한 테스트를 작성하는 데 집중할 것입니다.

 

작성할 테스트는 addNewTask 메서드를 호출할 때 새 작업 창을 열기 위한 이벤트가 발생하는지 확인합니다. 

테스트할 앱 코드는 다음과 같습니다.

 

TasksViewModel.kt

fun addNewTask() {
   _newTaskEvent.value = Event(Unit)
}

이 경우 newTaskEvent는 + FAB가 눌렸음을 나타내며 AddEditTaskFragment로 이동해야 합니다.

 

더보기

Event class : TO-DO 앱에서 사용자 정의 Event 클래스를 사용하여 LiveData가 일회성 이벤트(예: 탐색 또는 스낵바 팝업)를 나타내도록 합니다. Event LiveData는 TasksFragment에서 관찰됩니다.

 

1 단계.  TasksViewModelTest 클래스 만들기

TasksViewModel클래스에 있는 addNewTask() 함수를 호출할 때 Event() 함수가 잘 실행되는지 테스트하기위한 테스트 클래스를 만들기로 한다. 

Event() 함수는 새로운 task 창을 열기위한 함수입니다. 

 

 

 

TasksViewModel클래스 오른쪽 클릭 > generate>Test.클릭

 

Create Test 창에서 클래스 이름을 수정하고(선택) OK 클릭

로컬테스트를 할것이므로 "...\test\..."를 선택하고 OK 클릭

 

 

테스트 클래스 생성 완료

 

2 단계. ViewModel 테스트 작성 시작

addNewTest()함수 호출시 새로운 Task를 생성하기위한 새로운 창을 여는 Event()가 실행되는지 테스트하기 위한 테스트 code를 작성하고자 합니다. 

addNewTask_setsNewTaskEvent() 라는 새 테스트 code를 작성하십시오.

TasksViewModelTest.kt

class TasksViewModelTest {

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel

        // When adding a new task

        // Then the new task event is triggered
    }
}

 

이때 ViewModel 클래스의 경우 Application Context 객체가 필요하게되는데 

"AndroidX Test libraries"를 사용하면

simulated Android framework classes(가령 Application Context 등 )을 사용할 수 있습니다. 

 

TasksViewModelTest.kt

// Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(???)

AndroidX Test libraries를 사용하기위해서는

1. AndroidX Test와

2. Robolectric Testing library 종속성을 추가하고,
2. AndroidJunit4 runner @어노테이션을 작성하여

4. AndroidX 테스트 코드를 작성해야합니다. 

 

3 단계.  Gradle 종속성 추가 및  JUnit 테스트 runner 추가 후 AndroidX 테스트 수행

app/build.gradle 에 종속성을 추가하고

android {
	testOptions.unitTests {
		includeAndroidResources = true
		// ... 
	}
}

dependencies {
	// ....
	// AndroidX Test - JVM testing
	testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
	testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
	testImplementation "org.robolectric:robolectric:$robolectricVersion"
}

 

4 단계. 테스트 클래스 위에  JUnit 테스트 러너 어노테이션을 추가

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

dd

5단계  AndroidX 테스트 라이브러리를 사용

ApplicationProvider.getApplicationContext()를 사용하는 코드를 작성하면 다음과 같습니다.

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered

    }
}

 

 

정리  

  • 순수 VeiwModel의 경우 Android가 필요하지 않기때문에 보통 "test"  source set를 사용하여 테스트를 진행합니다. 
  • Applications and Activities와 같은 컴포턴트가 필요할때는 Androidx test library 를 사용하여 가져올 수 있습니다. 
  • "test" source set에서 시뮬레이션 된 Android 코드를 실행해야하는 경우 Robolectric 종속성 및 @RunWith(AndroidJUnit4::class)주석을 추가하면 실행이 가능합니다. 

 

 


AndroidX 테스트란 ?

AndroidX 테스트는 테스트 용 라이브러리 모음입니다. 여기에는 테스트를위한 애플리케이션 및 활동과 같은 구성 요소 버전을 제공하는 클래스 및 메서드가 포함됩니다. 

아래 예시 코드는 애플리케이션 컨텍스트를 가져 오기위한 AndroidX 테스트 함수의 예입니다.

ApplicationProvider.getApplicationContext()

 

AndroidX 테스트 API의 이점 중 하나는 로컬 테스트와 계측 테스트 모두에서 작동하도록 구축되었다는 것 입니다. 이것은 다음과 같은 이유로 좋습니다.

  • 로컬 테스트 또는 계측 테스트와 동일한 테스트를 실행할 수 있습니다.
  • 로컬 테스트와 계측 테스트에 대해 서로 다른 테스트 API를 배울 필요가 없습니다.

예를 들어 AndroidX 테스트 라이브러리를 사용하여 코드를 작성 했으므로 폴더 TasksViewModelTest에서 test폴더로 클래스를 이동할 수 androidTest있으며 테스트는 계속 실행됩니다. getApplicationContext()작품이 약간 다르게 로컬 또는 계측 테스트로 실행되고인지에 따라 :

  • 계측 테스트 인 경우 에뮬레이터를 부팅하거나 실제 장치에 연결할 때 제공되는 실제 애플리케이션 컨텍스트를 가져옵니다.
  • 로컬 테스트 인 경우 시뮬레이션 된 Android 환경을 사용합니다.

Robolectric 란?

로컬 테스트를 위해 사용되는 AndroidX 모의 안드로이드 환경이다. Robolectric은 테스트를 위해 시뮬레이션 된 Android 환경을 생성하고 에뮬레이터를 부팅하거나 기기에서 실행하는 것보다 빠르게 실행되는 라이브러리입니다.

 


Task 9 : LiveData에 대한 Assertions 작성

Writing Assertions for LiveData

 


1단계. InstantTaskExecutorRule 사용

InstantTaskExecutorRule는 JUnit 규칙입니다. 

@get:Rule주석 과 함께 사용 하면, 테스트 전후(@before, @after)에 InstantTaskExecutorRule 클래스의 일부 코드 가 실행됩니다.

이 규칙은 테스트 결과가 동기적으로 반복 가능한 순서로 발생하도록 동일한 스레드에서 모든 아키텍처 구성 요소 관련 백그라운드 작업을 실행합니다. LiveData 테스트를 포함하는 테스트를 작성할 때이 규칙을 사용하십시오!

이 규칙을 사용하기위해

app/build.gradle 에 "Architecture Components core testing" 라이브러리 종속성 추가 하고,

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"

TasksViewModelTest 클래스 내부에 다음을 추가합니다. 

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

 

ViewModel( for ApplicationContex) 테스트를 위한 AndroidX 라이브러리 추가와,

LiveData테스트를 위한 Architecture Components core testing (JUnit) 라이브러리를 추가한 코드 전체는 다음과 같습니다.

 

// AndroidX Test library ----- for ViewModel 테스트
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Architecture Components core testing library ( JUnit 규칙 ) ----- for LiveData 테스트
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered

    }
}

 

2 단계. LiveDataTestUtil .kt 클래스 추가

이제 드디어 테스트대상이 되는 LiveData가 관찰되고(observed) 있는지 확인하려합니다. 

보통 LiveData를 사용할때는 보통 activity/fragment(LifecycleOwner)가 LiveData를 관찰(observed)합니다. 

// activity 나 fragment에서 LiveData를observing 하는 코드

viewModel.resultLiveData.observe(fragment, Observer {
    // Observer code here
    
})

 

observation은 매우 중요한 기능을 합니다. 

onChanged events나 다른 어떤 변환을 트리거하기 위해서는 LiveData에 대한 active observers가 필요합니다.

 

뷰 모델의 LiveData에 대해 예상되는 LiveData 동작을 얻으려면 LifecycleOwner(activity/fragment)를 사용하여 LiveData를 관찰해야합니다.

하지만 이로 인해 문제가 발생합니다. TasksViewModel 테스트에서 LiveData를 관찰 할 activity/fragment가 없습니다.

이 문제를 해결하기 위해 ObservForever() 함수를 사용하면 LifecycleOwner 없이도 LiveData를 지속적으로 관찰 할 수 있습니다. ObservForever 함수를 사용하여 테스트 코드를 구현하면 다음과 같습니다. 

@Test
fun addNewTask_setsNewTaskEvent() {

    // Given a fresh ViewModel
    val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())


    // Observer 생성
    val observer = Observer<Event<Unit>> {}
    try {

        // LiveData를 계~~~속 Observe!
        tasksViewModel.newTaskEvent.observeForever(observer)

        // 새로운 task 추가됬을 때 ->> addNewTask() 함수 시작
        tasksViewModel.addNewTask()

        // 새로운 task event가 trigger됨
        val value = tasksViewModel.newTaskEvent.value
        assertThat(value?.getContentIfNotHandled(), (not(nullValue())))

    } finally {
        //observeForever를 사용하면 observer를 꼭 수동으로 제거해줘야함
        tasksViewModel.newTaskEvent.removeObserver(observer)
    }
}

 

하지만 ObservForever 를 수행 할 때 observer 를 제거하거나 observer leak 위험이 있음을 기억해야합니다.

ObservForever설명 ( DESTROYED상태일때 자동으로 observe가 해제되지 않음)

 

테스트에서 단일 LiveData를 observer하기 위해서 너무많은 boilerplate code(보일러플레이트 코드) 가 작성됩니다. 

observer를 더 간단하게 추가하기 위해 LiveDataTestUtil이라는 확장 함수를 만들 것입니다.
LiveDataTestUtil.kt라는 코틀린 파일을 만듭니다. 

// LiveData 객체에 getOrAwaitValue 확장함수 만듦

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // LiveData가 설정되지 않은 경우 무기한으로 기다리지 마세요
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

위 코드를 간단히 살펴보면 observer 를 추가하고 LiveData 객체에 observer 를 정리하는 getOrAwaitValue라는 Kotlin 확장 함수를 만듭니다. 기본적으로 위에 표시된 observeForever 코드의 짧고 재사용 가능한 버전입니다.

 

 

이제 위에서 구현한 getOrAwaitValue를 사용하여  assertion작성합니다.

TasksViewModelTest 의 최종 코드는 다음과 같습니다. 

addNewTask() 함수 호출시

newTaskEvent가 널값이 아닌지 테스트

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Test
    fun addNewTask_setsNewTaskEvent() {
        // 1) 새로운 ViewModel이 주어지면
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

         // 2) 새로운 task 추가됬을 때 ->> addNewTask() 함수 시작
        tasksViewModel.addNewTask()

        // 3) 새로운 task event가 trigger됨
        //   - 관찰할 라이브데이터 value를 가지고와서
        val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
        //   - 널값이 아닌지 확인
        assertThat(value.getContentIfNotHandled(), not(nullValue()))

    }

}

 


Task 10 : 추가적인 ViewModel 테스트 작성 예

Writing multiple ViewModel tests

 


 

setFiltering 함수에 대한 테스트 코드 작성

setFiltering 함수의 파라미터가 ALL_TASKS로 주어졌을때,

tasksAddViewVisible 값이 true 인지 테스트

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // 1) 새로운 ViewModel이 주어지면
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // 2)  필터 타입이 ALL_TASKS 일때 ->> setFiltering(TasksFilterType.ALL_TASKS) 함수 호출
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // 3) "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
    }

 

 

 

@ Before 주석을 사용하여 설정 메서드를 만들고 반복 된 코드를 제거 할 수 있습니다. 

 

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setupViewModel() {
         // 1) 새로운 ViewModel이 주어지면 (공통으로 수행되는 부분!!!)
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }


    @Test
    fun addNewTask_setsNewTaskEvent() {

         // 2) 새로운 task 추가됬을 때 ->> addNewTask() 함수 시작
        tasksViewModel.addNewTask()

        // 3) 새로운 task event가 trigger됨
        //   - 관찰할 라이브데이터 value를 가지고와서
        val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
        //   - 널값이 아닌지 확인
        assertThat(value.getContentIfNotHandled(), not(nullValue()))
    }


    @Test
    fun getTasksAddViewVisible() {

        // 2)  필터 타입이 ALL_TASKS 일때 ->> setFiltering(TasksFilterType.ALL_TASKS) 함수 호출
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // 3) "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
    }
    
}

 

 

 

 

 

728x90
반응형