CodeLab 05.1 Testing Basics
목차
- (Task 8) [ViewModel]테스트
- (Task 9) [LiveData] 테스트
- (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))
}
}
'Android > Android Test' 카테고리의 다른 글
[codelab 5.2 - 4, 5 ,6 ,7] Repository Unit Test (Fake Datasource with DI) (0) | 2021.05.05 |
---|---|
[codelab 5.2 - 3]Testing Strategy - 테스트 전략 개념 소개(테스트 피라미드, ) (0) | 2021.05.04 |
[codelab 5.1 - 5 6 7] Android Test 종류, [일반 클래스] Local Unit Test, TDD (0) | 2021.05.03 |
UI Test (with Espresso) (0) | 2021.04.28 |
Android Test관련 실습 예제 및 소스 목록 (1) | 2021.04.26 |