[Android TDD CodeLab 01. Testing Basics] part2

나고수·2022년 10월 23일
0

codelab

목록 보기
2/5

Android TDD CodeLab 01. Testing Basics Task. 8~14

Task: Setting up a ViewModel Test with AndroidX Test

TasksViewModel 테스트 - viewmodel에 모든 논리가 있고 repository 코드에 의존하지 않는 테스트

repository 코드에는 테스트 복잡성이 추가되는 비동기 코드, 데이터베이스 및 네트워크 호출이 포함됩니다. 지금은 이를 피하고 repository의 어떤 것도 직접 테스트하지 않는 ViewModel 기능에 대한 테스트 작성에 집중할 것입니다.

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

//event란?
//Event LiveData는 TasksFragment에서 관찰됩니다.
fun addNewTask() {
   _newTaskEvent.value = Event(Unit)
}

1단계. TasksViewModelTest 클래스 만들기 - local test

TasksViewModel에서 클래스 이름을 block하고 command+N 을 눌러서 테스트 클래스를 만듭니다.

  • 왜 viewmodel테스트가 로컬 테스트 인가요?
    viewModel 코드는 Android 프레임워크나 OS에 의존해서는 안 되므로 순수 viewModel 테스트는 일반적으로 test source set에 포함됩니다. 로컬 테스트로서 에뮬레이터나 장치가 아닌 로컬 시스템에서 테스트를 실행하기 때문에 더 빠르게 실행됩니다.

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

class TasksViewModelTest {

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel


        // When adding a new task


        // Then the new task event is triggered

    }
    
}

Application Context 얻기

테스트할 TasksViewModel 인스턴스를 만들 때 해당 생성자에는application context가 필요합니다. 그러나 이 테스트에서는 activity, UI 및 fragment로 application을 만들지 않습니다.
그렇다면 application context를 어떻게 얻습니까?

class TasksViewModel(application : Application) : ApplicationViewModel()

AndroidX Test libraries에는 테스트를 위한 Applications and Activities과 같은 구성 요소를 제공하는 클래스와 메서드가 포함되어 있습니다.
simulated Android framework classes(예: application context)가 필요한 로컬 테스트가 있는 경우 다음 단계에 따라 AndroidX 테스트를 올바르게 설정하십시오.
1. Add the AndroidX Test core and ext dependencies
2. Add the Robolectric Testing library dependency
3. Annotate the class with the AndroidJunit4 test runner
4. Write AndroidX Test code

3단계. Gradle dependencies 추가

app/build.gradle

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

4단계. Add JUnit Test Runner

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

5단계. Use AndroidX Test

// Given a fresh ViewModel
//이 시점에서 AndroidX 테스트 라이브러리를 사용할 수 있습니다. 
//여기에는 애플리케이션 컨텍스트를 가져오는 ApplicationProvider.getApplicationContext 메소드가 포함됩니다.

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

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
        //AndroidX test library에서 application context 가져옴

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

        // Then the new task event is triggered
        // TODO test LiveData
    }
}

Concept: How does AndroidX Test work?

AndroidX Test는 테스트용 라이브러리 모음입니다. 여기에는 테스트를 위한 Applications, Activities과 같은 components을 제공하는 클래스와 메서드가 포함됩니다.

  • 로컬 테스트와 계측 테스트 모두에서 작동하도록 빌드되었다.
  • 계측 테스트인 경우 에뮬레이터를 부팅하거나 실제 장치에 연결할 때 실제 application context가 제공됩니다.
  • 로컬 테스트인 경우 시뮬레이션된 Android 환경을 사용합니다.
  • robolectric란?
    AndroidX Test가 로컬 테스트에 사용하는 시뮬레이션된 Android 환경은 Robolectric에서 제공합니다.
    Robolectric은 테스트를 위해 시뮬레이션된 Android 환경을 생성하고 에뮬레이터를 부팅하거나 기기에서 실행하는 것보다 빠르게 실행되는 라이브러리입니다.

@RunWith(AndroidJUnit4::class)

testRunner이 있어야 Junit test가 된다.
기본적으로 제공하는 testRunner에서 다른 testRunner로 바꾸는 annotation
AndroidX 테스트를 사용하는 모든 클래스에서 사용됩니다.

Task: Writing Assertions for LiveData

Step 1. Use InstantTaskExecutorRule - liveData 작동을 위한 rule 추가

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    //이것은 JUnit에서 정의한 규칙이다.
    // 해당 클래스에서 발생하는 모든 아키텍처 컴포넌트(LiveData)의 백그라운드 실행을 하나의 스레드에서 작동시켜준다.
    // 그로 인해 thread safe 한 상태에서 테스트를 동작시킬 수 있다.
    //또한 동기적으로 반복 가능한 순서로 발생하도록 해준다.
    //live data 테스트 시 사용
    //testImplementation "androidx.arch.core:core-testing:$archTestingVersion" dependency 추가 해야함
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
   // Other code...}

Step 2. Add the LiveDataTestUtil.kt Class - observer 추가

  • 방법 1. observer forever 로 관찰하기 -> 마지막에 할당해제 해주는 것이 매우 중요❗️❗️
@Test
fun addNewTask_setsNewTaskEvent() {

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


    // Create observer - no need for it to do anything!
    val observer = Observer<Event<Unit>> {}
    try {
       //원래는 activity나 fragment의 생명주기가 livedata를 관찰한다.
       //하지만 테스트 시에는 activity나 Fragment의 생명주기를 가져 올 수 없다.
       //그래서 observeForever를 사용한다.
       //observer the livedata forever

        // Observe the LiveData forever
        tasksViewModel.newTaskEvent.observeForever(observer)

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

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.value
        assertThat(value?.getContentIfNotHandled(), (not(nullValue())))

    } finally {
        // Whatever happens, don't forget to remove the observer!
        tasksViewModel.newTaskEvent.removeObserver(observer)
    }
}
  • 방법 2. LiveDataTestUtil.kt 을 만들어서 boilerplate code 지우기
package com.example.android.architecture.blueprints.todoapp

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

//livedata 관찰을 위한 observer를 더 쉽게 만들기 위한 class
//test source set -> tasks 안에 만들기 
//observe을 추가하고 LiveData 값을 가져온 다음 observer를 정리하는 getOrAwaitValue라는 Kotlin 확장 함수를 생성합니다.

    @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()

            // Don't wait indefinitely if the LiveData is not set.
            if (!latch.await(time, timeUnit)) {
                throw TimeoutException("LiveData value was never set.")
            }

        } finally {
            this.removeObserver(observer)
        }

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

Step 3. Use getOrAwaitValue to write the assertion

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()


    @Test
    fun addNewTask_setsNewTaskEvent() {
        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

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

        // Then the new task event is triggered
        //observe forever 없이 LiveDataTestUtil에서 정의한 대로 livedata 값 가져옴
        val value = tasksViewModel.newTaskEvent.getOrAwaitValue()

		////event이기 때문에 getContentIfNotHandled로 value 가져오기
        assertThat(value.getContentIfNotHandled(), not(nullValue()))
    }

Task: Writing multiple ViewModel tests

모든 작업을 표시하도록 필터 유형을 설정한 경우 작업 추가 버튼이 표시되는지 확인하는 테스트

package com.example.android.architecture.blueprints.todoapp.tasks

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.Event
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.CoreMatchers.*
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

//testRunner이 있어야 Junit test가 된다. 기본적으로 제공하는 testRunner에서 다른 testRunner로 바꾸는 annotation
//AndroidX 테스트를 사용하는 모든 클래스에서 사용됩니다.
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    //이것은 JUnit에서 정의한 규칙이다.
    // 해당 클래스에서 발생하는 모든 아키텍처 컴포넌트(LiveData)의 백그라운드 실행을 하나의 스레드에서 작동시켜준다.
    // 그로 인해 thread safe 한 상태에서 테스트를 동작시킬 수 있다.
    //또한 동기적으로 반복 가능한 순서로 발생하도록 해준다.
    //live data 테스트 시 사용
    //testImplementation "androidx.arch.core:core-testing:$archTestingVersion" dependency 추가 해야함
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    private lateinit var tasksViewModel: TasksViewModel

    @Before //👈여러 테스트에 대해 반복된 설정 코드가 있는 경우 @Before 주석을 사용하여 설정 방법을 만들고 반복되는 코드를 제거할 수 있습니다.
    fun setUpViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel
//        val tasksViewModel =
//            TasksViewModel(ApplicationProvider.getApplicationContext()) //AndroidX test library에서 application context 가져옴

        val observer = Observer<Event<Unit>> {}
        try {
            //원래는 activity나 fragment의 생명주기가 livedata를 관찰한다.
            //하지만 테스트 시에는 activity나 Fragment의 생명주기를 가져 올 수 없다.
            //그래서 observeForever를 사용한다.
            //observer the livedata forever
            tasksViewModel.newTaskEvent.observeForever(observer)
            // When adding a new task
            tasksViewModel.addNewTask()
            //then the new task event is triggered
            val value = tasksViewModel.newTaskEvent.value
            assertThat(value?.getContentIfNotHandled(), (not(nullValue())))
        } finally {
            // Whatever happens, don't forget to remove the observer!
            tasksViewModel.newTaskEvent.removeObserver(observer)
        }
    }

    @Test
    fun addNewTask_setsNewTaskEvent_with_util() {

        // Given a fresh TasksViewModel
//        val tasksViewModel =
//            TasksViewModel(ApplicationProvider.getApplicationContext()) //AndroidX test library에서 application context 가져옴
        tasksViewModel.addNewTask()
        val value =
            tasksViewModel.newTaskEvent.getOrAwaitValue() //observe forever 없이 LiveDataTestUtil에서 정의한 대로 livedata 값 가져옴
        assertThat(
            value.getContentIfNotHandled(), not(nullValue())
        ) //event이기 때문에 getContentIfNotHandled로 value 가져오기
    }

    @Test //👈 테스트 코드 !!!
    fun setFilterAllTasks_tasksAddViewVisible() {
        // Given a fresh ViewModel
        // val taskViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
        // Then the "Add task" action is visible
        val value = tasksViewModel.tasksAddViewVisible.getOrAwaitValue()
        assertThat(value, `is`(true))
    }

}
profile
되고싶다

0개의 댓글