저번 글에 이어서 안드로이드에서 어떻게 테스팅을 하는지 codelabs를 따라하며 배워보자.
저번글에서는 안드로이드 구성요소가 전혀 없는 유닛 테스트를 해봤다.
이번 글에서는 안드로이드 구성요소가 포함된 간단한 유닛 테스트를 해본다.
본격적으로 글을 시작하기 전에 Hamcrest라는 테스팅 라이브러리를 간단히 살펴본다.
Hamcrest를 사용하면 조금더 사람 언어(그래봤자 영어지만)에 가까운 테스트케이스를 작성할 수 있다.
사실 영어권 사람이 아니면 가독성에 크게 공감하지 못할 수 있긴 하다.
하지만 많은 테스트 예제나 실제 테스트들에서 Hamcrest를 사용하므로 Hamcrest를 사용한다.
우선 아래와 같이 App Module의 gradle 파일의 dependencies에 추가한다.
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
Hamcrest를 사용하면 아래와 같이 작성할 수 있다.
// default JUnit
assertEquals(result.completedTasksPercent, 0f)
// Hamcrest
assertThat(result.completedTasksPercent, `is`(0f))
즉 앞으로는 I assert that A is (not) B
이런 방식으로 assert
문을 작성할 수 있다.
(그냥 is
가 아니라 `를 씌운 이유는 코틀린에서 is는 이미 예약어로 쓰이고 있기 때문이다.)
위에서 본 is
말고도 assertThat()
에 이어쓸 충분히 많은 함수가 있으므로 Hamcrest Tutorial을 참고하자.
이번에 테스팅 할 클래스 / 함수는 아래와 같다.
class TasksViewModel(application: Application) : AndroidViewModel(application) {
private val _newTaskEvent = MutableLiveData<Event<Unit>>()
val newTaskEvent: LiveData<Event<Unit>> = _newTaskEvent
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
}
TasksViewModel
은 AndroidViewModel
을 상속한 클래스이기 때문에 Application
을 받았다.
addNewTask()
는 특별한 기능 없이 새로운 Task
가 들어왔다고 이벤트를 전달한다.
여기서 addNewTask를 했을 때 정상적으로 이벤트가 발생하는지 테스팅해보자.
TasksViewModel
를 보면 알겠지만 Application
이 없으면 아예 생성할 수 없다. 이전 글에서 테스팅한 클래스나 함수는 코틀린 언어만 사용할 줄 할면 만들 수 있는 클래스였다. 하지만, Application
클래스는 안드로이드 구성요소로 안드로이드 프레임워크의 도움 없이는 만들 수 없다.
그러면 테스팅을 위해 안드로이드 구성요소를 사용하는 방법에 대해 알아보자.
AndroidX Test
와 Robolectrics
의 관련 의존성을 추가하면 Test 디렉토리에서 simulated Android framework classes를 사용할 수 있다.
(androidTest 디렉터리에서 같은 코드를 실행하면 그때는 실제 class를 사용한다.)
아래와 같이 gradle 파일에 의존성을 추가하자.
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
이제 테스트 클래스와 테스트 함수를 작성해야하는데 전 글에서의 테스트와 달리 테스트 클래스에 어노테이션을 추가해야한다.
먼저 Test 클래스에 @RunWith(AndroidJUnit4::class)
어노테이션을 붙여줘야한다.
사실 JUnit을 사용하는 테스팅은 TestRunner
가 필수적으로 필요하다.
이전에 봤던 예제에는 @RunWith
어노테이션을 사용하지 않았는데, 이는 기본 TestRunner를 써도 상관없었기 때문이다.
이제 우리는 AndroidXTest
를 사용하기 때문에 @RunWith(AndroidJUnit4::class)
를 추가한다.
이 어노테이션을 추가함으로써 AndroidXTest
는 UI를 포함한 androidTest인지 일반 Test인지 구분하여 작동할 수 있다.
그리고 우리가 원했던 Application
클래스를 아래와 같이 가져올 수 있다.
ApplicationProvider.getApplicationContext()
이제 생성자의 파라미터 Application
를 넘겨 TasksViewModel
을 만들 수 있다.
최종적으로 @Test
함수의 given을 다음과 같이 작성할 수 있을 것이다.
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When
// Then
}
LiveData
인 newTaskEvent
를 테스팅하기 전에 JUnit Rule을 추가한다.
(JUnit Rule은 test 전 / 후에 정의된 코드들이 실행되도록 만든다.)
여기서는 InstantTaskExecutorRule
를 사용한다.
InstantTaskExecutorRule
을 이용하면 안드로이드 구성요소 관련 작업들을 모두 한 스레드에서 실행되게 한다.
그러므로 모든 작업이 synchronous하게 동작하여 테스팅을 원활하게 할 수 있다.
즉 동기화 때문에 고민할 필요가 없어진다.
특히 LiveData를 이용한다면 필수적으로 InstantTaskExecutorRule
를 사용해야할 것이다.
아래와 같이 dependencies를 추가하고.
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
테스트 코드에 적용하자.
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
}
LiveData를 사용하는 경우 99%의 경우 다음 두 가지가 필요하다.
liveData.observe(lifecycleOwner, Observer{ ... })
하지만, 우리가 현재 하려는 테스트에 Activity나 Fragment는 없다.
그러므로 여기서는 LiveData를 lifecycle과 무관하게 쓸 수 있는 observeForever()
를 사용한다.
observeForever()
를 사용하기 때문에 lifecycle과 무관하게 옵저버 패턴을 사용할 수 있다.
최종적으로는 아래와 같은 테스트 클래스가 될 것이다. (codelabs의 코드다.)
@Test
fun addNewTask_setsNewTaskEvent() {
// Given : new TasksViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
try {
// Observe the LiveData forever
tasksViewModel.newTaskEvent.observeForever { }
// When : add a new task
tasksViewModel.addNewTask()
// Then : the new task event is triggered
val value = tasksViewModel.newTaskEvent.value
assertThat(value?.getContentIfNotHandled(), (not(nullValue())))
} finally {
// lifecycle unaware이므로 직접 제거한다.
tasksViewModel.newTaskEvent.removeObserver(observer)
}
}
사실 위 코드는 LiveData를 테스팅할 때 마다 등록 / 취소를 해야해서 보일러플레이트 코드가 생긴다.
그래서 codelabs에서는 LiveData<T>.getOrAwaitValue
를 사용한다.
함수에 대한 자세한 설명이 필요하면 링크에서 찾아보면 쉽게 이해할 수 있을 것이다.
지금까지 안드로이드 구성요소를 포함한 유닛테스트를 작성해봤다.
안드로이드 구성요소들은 개발자가 직접 컨트롤 하기 어렵다.
그러므로 라이브러리의 도움이 없었으면 정말 힘든 작업이었을 것이다.
이제 AndroidX Test Library를 사용해 안드로이드 구성요소를 포함한 유닛테스트도 잘 활용해보자.
다음에는 한 클래스(여기서는 TasksViewModel)만 포함된 테스트가 아닌, 여러 클래스를 복합적으로 테스트해볼 것이다.