1. TDD - 안드로이드 예제와 함께 TDD 를 배워보자!
2. 목차
2.1. TDD - Test Driven Development
2.2. TDD 의 장점
2.3. Android App에서의 TDD
2.3.1. Instrumented vs local test
2.3.2. 예제
2.4. 예제와 함께하는 Junit 테스트 공부
2.4.1. step 1. 로컬 테스트 실행하기
2.4.2. step 2. 로컬 테스트 실패하기
2.4.3. step 3: Instrumented test 실행하기
2.5. 첫 번째 테스트 코드를 작성해보자
2.5.1. step 1. 테스트 클래스를 생성하자.
2.5.2. step 2. 테스트 함수를 작성하자
2.6. 더 많은 테스트 코드를 작성해보자
2.6.1. step 1. 테스트 작성하기
2.6.2. step 2. 버그를 발생시킬 테스트 코드를 작성하기
2.6.3. step 3. 버그를 수정하기
2.7. AndroidX Test를 활용한 ViewModel Test 작성하기
2.7.1. Step 1. TasksViewModelTest class 생성하기
2.7.2. Step 2. ViewModel Test 코드를 작성해보자
2.7.3. Step 3. JUnit Test Runner 추가하기
2.7.4. Step 4. Use AndroidX Test
2.8. AndroidX Test 의 컨셉!
2.8.1. AndroidX Test 가 뭔가요?
2.8.2. What is Robolectric?
2.8.3. @RunWith(AndroidJUnit4::class) 는 무슨 역할을 하는 코드일까?
2.8.4. step 5. Robolectric Warnings 해결하기
2.9. LiveData를 향한 Assertion 작성하기
2.9.1. step 1. InstantTaskExecutorRule 사용하기
2.9.2. step 2. LiveDataTestUtil.kt 클래스 추가하기
2.9.3. step 3. getOrAwait 확장 함수를 사용해 assertion 하기
2.10. 다양한 ViewModel Test 작성해보기
2.10.1. step 1. 테스트 코드 작성하기
2.10.2. step 2. solution 코드와 비교하기
2.10.3. step 3. @Before rule 추가하기
이 글은 https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-basics#4 예제를 보고 따라하면서 작성한 글입니다.
1. TDD - 안드로이드 예제와 함께 TDD 를 배워보자!
작성일자: 2021년 12월 26일 오후 10:42
작성자: HoJong
2. 목차
2.1. TDD - Test Driven Development


TDD란 Test Driven Development의 약자로, ‘테스트 주도 개발’ 이라고 명합니다. 기존의 개발 프로세스가 디자인 → 개발 → 테스트 순서였다면, TDD 는 개발에 앞서 테스트케이스를 작성하는 프로세스를 가집니다.
즉 테스트 코드를 미리 작성하여 결과를 예상해볼 수 있어, 설계의 문제로 인한 오류를 더욱 빠르게 개선할 수 있습니다.
2.2. TDD 의 장점
- 객체지향적인 코드 작성
- 테스트 코드를 먼저 작성한다면 더 명확한 기능과 구조를 설계할 수 있습니다. → 테스트의 용이성을 위해 복잡한 기능을 한 함수에 모두 구현할 경우, 테스트 방식이 복잡해지고 시간이 오래 걸리기 때문에, 자연스럽게 재사용성을 보장하며 코드를 작성하게 된다.
- 설계 수정 시간 단축
- 테스트 코드를 먼저 작성하기 때문에, 설계의 입출력 구조와 기능의 정의를 명확히 하게 되므로 설계의 구조적인 문제를 바로 찾아낼 수 있다.
- 디버깅 시간 단축
- 단위 테스트 기반의 테스트 코드를 작성하기 때문에, 문제가 발생하였을 때 모듈 별로 테스트를 진행해 문제의 원인을 쉽게 찾아낼 수 있다. 프로그램의 오류는 Application 영역, Data 영역, DB 영역 등 다양한 곳에서 발생할 수 있기 때문에, 실제 프로그램으로 해당 오류를 재현시키기는 매우 어렵다. 그러나 단위 테스트를 진행하게 되면, 영역을 분할하여 오류의 원인을 쉽게 찾을 수 있다.
- 테스트 문서의 대체 기능
- 기존의 개발 프로젝트 진행 시 테스트를 진행하는 경우는 단순히 Application 의 통합 테스트에 지나지 않는다. 즉 테스트 과정을 통해서 내부적으로 개별적인 모듈들이 어떻게 테스트 되었는지 알 수 없다. 그러나 TDD 를 사용시 테스트를 자동화 시킴으로써 모듈들이 어떻게 테스트되었는지 제공할 수 있다.
2.3. Android App에서의 TDD
모바일 어플리케이션은 매우 복잡하고 다양한 환경에서 정상적으로 작동해야 합니다. 그러므로 어플리케이션 테스트엔 매우 많은 종류가 있습니다.
주제
- Funtional testing : 어플리케이션이 설계한 대로 동작하는가?
- Performance testing : 효율적이고 빠르게 동작하는가?
- Accessibility testing : 다양한 접근성 서비스와 정상적으로 작동하는가?
- Compatibility testing : 타겟으로 지정한 모든 디바이스와 API 레벨에서 정상적으로 작동하는가?
범위
- Unit tests 또는 small tests : 메서드와 클래스 같이 작은 부분만 증명하는 테스트
- End-to-end test 또는 big tests : 화면이나 유저 플로우와 같은 큰 단위의 테스트
- Medium tests : 두 개 이상의 단위들의 통합을 확인하는 테스트

2.3.1. Instrumented vs local test
테스트는 안드로이드 기기 혹은 다른 컴퓨터에서 수행할 수 있습니다.
- Instrumented test는 실제 혹은 가상의 안드로이드 기기에서 동작하는 테스트입니다. 실제 앱과 테스트 앱이 동시에 설치되는데, 주로 앱을 실제로 실행하고 사용자와 상호작용하는 UI 테스트에 사용됩니다.
- Local test는 개발 기기나 서버에서 실행되는 테스트로, host-side test라고도 불립니다. 주로 작고 빠른 테스트들인데, 나머지 앱과 분리된 테스트들을 의미합니다.

2.3.2. 예제
다음 코드 스니펫은 Instrumented test에서 UI 테스트의 예제를 설명합니다.
// When the Continue button is clicked
composeTestRule.onNodeWithText("Continue").performClick()
// Then the Welcome screen is displayed
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
다음 코드 스니펫은 뷰모델에서 단위 테스트의 예제를 설명합니다.
// Given an instance of MyViewModel
val viewModel = MyViewModel(myFakeDataRepository)
// When data is loaded
viewModel.loadData()
// Then it should be exposing data
assertTrue(viewModel.data != null)
2.4. 예제와 함께하는 Junit 테스트 공부
https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-basics#4
위 링크로 들어가, github 저장소를 clone 하거나, 안드로이드 프로젝트를 다운받아 안드로이드 스튜디오로 프로젝트를 열자.
안드로이드 프로젝트를 새로 생성하면, 다음과 같이 3개의 패키지가 생성될 것이다.

com.example.android.architecture.blueprints.todoapp
com.example.android.architecture.blueprints.todoapp (androidTest)
com.example.android.architecture.blueprints.todoapp (test)
이 폴더들을 soruce sets라고 부른다. source sets은 앱의 소스코드를 포함하고 있는 폴더를 말한다. 초록색으로 표시된 source sets (androidTest, test) 은 테스트 코드를 포함한다. 새로운 프로젝트를 생성하면 세 개의 기본적인 source set를 볼수 있다.
main
: 어플리케이션 코드를 포함한다.androidTest
: Instrumented test (Android emulator 혹은 실제 기기에서 동작하는) 코드를 포함한다- 실제 안드로이드 기기 혹은 에뮬레이터에서 동작하므로, 실제 어플리케이션이 동작하는 것처럼 동작한다. 그러나 그러므로 느리다.
test
: local (개발 도구에서 동작하는) 테스트 코드를 포함한다.- 개발 도구의 JVM 상에서만 동작하는 테스트로, 에뮬레이터나 실제 안드로이드 기기를 필요로 하지 않는다. 따라서 빠르지만, 실제 어플리케이션에서 동작하는 것과는 거리가 있음
2.4.1. step 1. 로컬 테스트 실행하기
- test source set을 열어 ExampleUnitTest.kt 파일을 열어보도록 합니다.
- ExampleUnitTest.kt를 우클릭 해 Run ExampleUnitTest를 클릭합니다.

addition_isCorrect()
테스트가 통과해 초록색 체크마크가 표시된 것을 볼 수 있다.
2.4.2. step 2. 로컬 테스트 실패하기
아래 코드는 우리가 방금 실행한 테스트 코드다.
// A test class is just a normal class
class ExampleUnitTest {
// Each test is annotated with @Test (this is a Junit annotation)
@Test
fun addition_isCorrect() {
// Here you are checking that 4 is the same as 2+2
assertEquals(4, 2 + 2)
}
}
위 코드를 살펴보면 테스트는 다음과 같다는 것을 확인할 수 있다.
- 테스트는 test source set에 있는 하나의 클래스이다.
- @Test 어노테이션으로 시작하는 함수가 하나의 테스트가 된다.
- 대개 assertion statement 를 포함한다.
안드로이드는 JUnit 테스트 라이브러리를 사용한다. 위 assertion statement 와 @Test 어노테이션은 모두 JUnit이 지원하는 코드다.
assertion이 위 테스트 코드에서 핵심인 부분이라 할 수 있다. 어플리케이션이 정상적으로 동작한지 확인하는 구문으로, 이 경우에 assertEquals(4 , 2 + 2) 는 4 가 2+2와 동일한지 확인하는 구문이다. 그렇다면 테스트가 실패했을 경우를 보기 위해서 assertEquals(3, 1 + 1) 코드를 추가해보자.
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
assertEquals(3, 1 + 1) //이 테스트는 실패해야한다!
}
그 다음 위 테스트를 다시 실행해보자. 그렇다면 다음과 같은 결과를 얻게 된다.

당연히 테스트는 실패하게 되고, 다음과 같은 사실을 확인할 수 있다.
- 하나의 assertion fail은 전체 테스트의 실패로 이어진다.
- 실패한 assertion 코드를 IDE 에서 바로 알려준다. (ExampleUnitTest.kt : 16)
2.4.3. step 3: Instrumented test 실행하기
Instrumented test는 androidTest source set
에 있다.
androidTest source set
을 열자ExampleInstrumentedTest
을 실행해보자!
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("per.hojong.todo", appContext.packageName)
}
}
local test와는 다르게, 실제 기기에서 테스트가 실행된 것을 확인할 수 있다.

2.5. 첫 번째 테스트 코드를 작성해보자
2.5.1. step 1. 테스트 클래스를 생성하자.
main
source set 에서,todoapp.statistics
패키지를 찾아StatisticsUtils.kt
.파일을 열자getActiveAndCompletedStats
함수를 찾자.
StatisticsUtils.kt
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
val totalTasks = tasks!!.size
val numberOfActiveTasks = tasks.count { it.isActive }
val activePercent = 100 * numberOfActiveTasks / totalTasks
val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks
return StatsResult(
activeTasksPercent = activePercent.toFloat(),
completedTasksPercent = completePercent.toFloat()
)
}
data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)
getActiveAndCompletedStats
함수는 Task 클래스의 리스트를 받아 StatsResult
클래스를 반환하는 함수다. StatsResult
는 완료된 태스크의 비율과 진행 중인 태스크의 비율을 가지고 있는 data class다.
- 이제 이 함수를 우클릭 해서, Generate → Test 를 클릭해보자.


- class name 을
StatisticsUtilsKtTest
대신StatisticsUtilsTest
로 변경한 뒤에, OK 를 눌러 테스트 코드를 생성하자.

- 위 함수는 Android 프레임워크의 그 어떤 것도 사용하지 않기 때문에, 우리는
test source set
에다가 위 테스트 코드를 생성할 것이다. - 이제
test source set
을 들어가면,statistics
패키지 하위에StatisticsUtilTest
클래스가 생성된 것을 확인할 수 있다.
2.5.2. step 2. 테스트 함수를 작성하자
이제 다음과 같은 테스트 코드를 작성할 것이다.
- 하나의 진행중인 태스크와 0개의 완료된 태스크가 주어진다면,
- active task의 비율은 100% 고
- completed task 의 비율은 0%인 것을 검증하는 테스트 코드 작성
StatisticsUtilsTest
클래스를 열자getActiveAndCompletedStats_noCompleted_returnsHundredZero
함수를 작성하자.
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
}
- 위에 설명한 것과 같은 Task 리스트를 생성하자!
val tasks = listOf<Task>(Task("title", "desc", isCompleted = false))
- 이제
getActiveAndCompletedStats
함수를 실행해 그 결과를 저장하자.
val result = getActiveAndCompletedStats(tasks)
assertEquals
를 사용해 비율을 검증하자.
val result = getActiveAndCompletedStats(tasks)
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, `is`(100f))
전체 코드는 다음과 같다.
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
// Create an active task (the false makes this active)
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
// Call your function
val result = getActiveAndCompletedStats(tasks)
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
}
}
- 이제 Test를 실행해보자. 정상적으로 통과되었다는 결과를 얻을 것이다!
2.6. 더 많은 테스트 코드를 작성해보자
이 예제 공부는 선택적이므로, 꼭 따라할 필요는 없다. 이 예제에서 JUnit 을 활용해 더 많은 테스트 코드 작성법을 배울 것이다. 또한 feature code보다 test code를 먼저 작성하는, TDD 전략에 따라 코드를 작성하는 법을 배울 것이다.
2.6.1. step 1. 테스트 작성하기
먼저 다음과 같은 두 개의 테스트 코드를 작성해보자
- 하나의 완성된 task 와 0개의 진행중인 task 가 존재한다면, activeTask 비율은 0%가 되고 completed task 비율은 100%가 된다.
- 2 개의 완료된 task 와 3개의 진행중인 task 가 존재한다면, completed task 비율은 40%가 되고 active task 비율은 60%가 된다.
코드는 다음과 같다. 스스로 작성하고 확인해보자!
@Test fun getActiveAndCompletedStats_oneCompleted() { val tasks = listOf<Task>( Task("title", "desc", isCompleted = true), ) val result = getActiveAndCompletedStats(tasks) assertEquals(result.completedTasksPercent, 100f) assertEquals(result.activeTasksPercent, 0f) } @Test fun getActiveAndCompletedStats_2Completed_3Active() { val tasks = listOf<Task>( Task("title", "desc", isCompleted = false), Task("title", "desc", isCompleted = false), Task("title", "desc", isCompleted = true), Task("title", "desc", isCompleted = true), Task("title", "desc", isCompleted = false), ) val result = getActiveAndCompletedStats(tasks); assertEquals(result.completedTasksPercent, 40f) assertEquals(result.activeTasksPercent, 60f) }
2.6.2. step 2. 버그를 발생시킬 테스트 코드를 작성하기
사실 위에서 테스트 코드 대상으로 사용된 함수인 getActiveAndCompletedStats
함수는 버그를 가지고 있다! 만약 매개변수로 넘어오는 List가 비거나 null 인 경우면 어떻게 될까? 정상적인 결과는 activated 비율과 completed 비율이 전부 0%가 되어야 한다. 그러나 위 함수는 null 값에 대한 처리나 리스트가 비었을 때 처리를 따로 해주지 않았다.
따라서 0으로 나누는 코드로 인해 ArithmeticException 혹은 null 인 리스트로 인해 NullPointerException 이 발생할 것으로 예측된다. 실제로 위의 상황을 가정해서 테스트 코드를 작성해보자
코드는 다음과 같다. 스스로 작성하고 확인해보자!
@Test fun getActiveAndCompletedStats_Empty_List() { val tasks = listOf<Task>() val result = getActiveAndCompletedStats(tasks) assertEquals(result.completedTasksPercent, 0f) assertEquals(result.activeTasksPercent, 0f) } @Test fun getActiveAndCompletedStats_Null_List() { val tasks: List<Task>? = null val result = getActiveAndCompletedStats(tasks) assertEquals(result.completedTasksPercent, 0f) assertEquals(result.activeTasksPercent, 0f) }
위 두개의 테스트를 실행하면, 역시 테스트 결과에 실패하는 것을 확인할 수 있다.

그렇다는 뜻은 우리가 실제 기능을 수행하는 함수를 잘못 작성했다는 것이다! 이같은 과정을 수행함으로써 우리는 TDD를 적용했다고 할 수 있다. 왜냐하면 TDD 는 다음과 같은 과정을 따르기 때문이다.
[Given - when - then](https://brunch.co.kr/@springboot/292)
방식을 사용해 테스트 코드를 작성한다. (모르면 링크 클릭하셈)- 테스트가 실패하는 것을 확인한다! (아직 실제 기능을 수행하는 함수를 작성하지 않았으므로)
- 테스트를 통과하기 위해 실제 코드를 작성한다.
- 모든 테스트를 계속 반복한다.

위와 같이 우리가 생각할 수 있는 예외들에 대해 테스트 케이스를 계속해서 작성한다. 미래에 해당 기능에 대한 코드 수정으로 기능을 검증할 필요가 생겼을 때, 간단히 작성해둔 테스트 케이스를 사용해서 코드를 쉽게 검증할 수 있을 것이다.
2.6.3. step 3. 버그를 수정하기
이제 위 테스트 케이스로 인해 인지한 버그를 수정해보자!
- 빈 리스트나 null 인 리스트를 인자로 넘겼을 때, 두 비율이 모두 0%가 되도록
getActiveAndCompletedStats
함수를 수정해보자.
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
return if (tasks == null || tasks.isEmpty()) {
StatsResult(0f, 0f)
} else {
val totalTasks = tasks!!.size
val numberOfActiveTasks = tasks.count { it.isActive }
StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
}
}
- 버그를 수정했으면, 다시 한 번 테스트를 실행해 코드를 검증하자

- 새로운 기능은 항상 관련된 함수가 존재하므로, test는 코드가 무슨 역할을 하는지와 같은 문서의 역할도 수행한다.
- 테스트는 우리가 이미 발견한 버그에 대한 테스트를 수행한다. → 따라서 버그 발견 시 이를 테스트 케이스로 작성하는 과정이 중요할 것 같다.
2.7. AndroidX Test를 활용한 ViewModel Test 작성하기
이제 나머지 예제에서 Android 클래스 중 가장 많이 사용하는 ViewModel
과 LiveData
에 대해 테스트 코드를 작성하는 법을 배울 것이다. 이 예제에선 TasksViewModel
을 사용한다.
일단 repository 에 의존하지 않고 모든 기능을 스스로 가지고 있는 viewModel 에 대해서 다뤄볼 것이다. repository 코드는 비동기 처리, db 와 네트워크 이벤트를 포함할 수 있는데, 이것은 test 를 복잡하게 하기 때문이다. 지금 당장은 쉬운 이해를 위해 repository 를 배제하도록 하자.

우리가 작성할 테스트 코드는 addNewTask 함수를 호출했을 때, 새로운 Task 작성을 위한 창이 떳는지 확인하는 코드다. 아래는 우리가 테스트할 어플리케이션 코드다.
TasksViewModel.kt
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
2.7.1. Step 1. TasksViewModelTest class 생성하기
위에서 StatisticsUtilTest를 작성할 때와 동일한 방법으로 TasksViewModelTest 테스트 클래스를 생성해보자. 이 때 테스트 클래스를 test source set
에 생성하자!
2.7.2. Step 2. ViewModel Test 코드를 작성해보자
이 단계에선 addNewTask() 함수 호출 시 새 창 열기를 위한 이벤트가 발생되는지 테스트하는 코드를 작성할 것이다.
- addNewTask_setsNewTaskEvent 함수를 생성하자
TasksViewModelTest.kt
class TasksViewModelTest {
@Test
fun addNewTask_setsNewTaskEvent() {
//Given a fresh TasksViewModel
//When adding a new task
//then the new task event is triggered
}
}
여기서 주의할 점이 있다! TasksViewModel 은 AndroidViewModel로, 생성할 때 application Context
가 필요하다. 그러나 이 테스트에선 activity 나 UI 프래그먼트 같은 전체 어플리케이션을 생성하지 않는다. 그럼 어떻게 application Context
를 얻을 수 있을까?
//Given a fresh TasksViewModel
val tasksViewModel = TasksViewModel(???)
AndroidX Test 라이브러리는 Application 이나 Activity 같은 안드로이드 컴포넌트를 제공하는 클래스나 메소드를 포함한다. Android 프레임워크 요소 (application Context
같은)가 필요한 Local Test를 진행할 때, 다음과 같은 과정을 진행해 AndroidX Test를 설정해보자.
- AndroidX Test 의존성을 추가하자
- Robolectric Testing library 의존성을 추가하자
- AndroidJunit4 어노테이션을 클래스에 추가하자
- AndroidX 테스트 코드를 작성하자.
다음 의존성을 앱 수준의 build.gradle
파일에 추가하자
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
2.7.3. Step 3. JUnit Test Runner 추가하기
- @RunWith(AndroidJUnit4::class) 어노테이션을 테스트 클래스위에 추가하자.
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Test code
}
2.7.4. Step 4. Use AndroidX Test
AndroidX Test 라이브러리는 ApplicationContext 제공자를 가지고 있다.
ApplicationProvider.getApplicationContext()
를 활용해TasksViewModel
을 생성하자!
@Test
fun addNewTask_setsNewTaskEvent() {
//Given a fresh TasksViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
//When adding a new task
//then the new task event is triggered
}
- addNewTask() 함수를 호출하자!
tasksViewModel.addNewTask()
2.8. AndroidX Test 의 컨셉!
2.8.1. AndroidX Test 가 뭔가요?
AndroidX Test는 테스트를 위한 라이브러리의 모음입니다. Android Component를 제공하기 위한 클래스나 메소드들을 포함하고 있으며, 이것들은 모두 테스트를 위한 요소들입니다. 위 예제에서 보았듯이 우리는 AndroidX Test를 추가해 Application Context
를 얻을 수 있습니다.
AndroidX Test API 를 사용함으로써 우리는 Local test와 Instrumented Test를 동시에 사용할 수 있습니다.
- 동일한 테스트를 Local Test 혹은 Instrumented Test로 실행할 수 있다.
- 다른 API 를 배울 필요가 없다
AndroidX Test 라이브러릴 사용했으므로, TasksViewModel 을 test source set 에서 androidTest source set으로 이동해도 여전히 동작합니다.
- Instrumented test 일 경우 실제 Android component를 제공해줍니다.
- Local test일 경우 가상 안드로이드 환경의 component를 제공해줍니다.
2.8.2. What is Robolectric?
AndroidX TEst가 사용하는 가상 Android 환경은 Robolectric
에 의해 제공됩니다. Robolectric
은 안드로이드 가상 환경을 제공하는 라이브러리로, 에뮬레이터나 실제 기기를 사용해 테스트하는 것보다 빠릅니다.
2.8.3. @RunWith(AndroidJUnit4::class) 는 무슨 역할을 하는 코드일까?
test runner 는 JUnit 컴포넌트이다. test runner 가 없으면 테스트 코드를 실행할 수 없다. JUnit에서 기본적으로 제공하는 test runner 가 있는데, @Runwith 어노테이션은 이 test runner를 바꾸는 역할을 한다.
따라서 AndroidJUnit4 test runner 는 Local Test 혹은 Instrumented Test인지에 따라서 Android component 를 제공해준다.
2.8.4. step 5. Robolectric Warnings 해결하기
이제 다시 예제로 돌아와서, 테스트 코드를 실행해보자. 그러면 No such manifest file:./AndroidManifest.xml
에러를 볼 수 있는데, app 수준의 build.gradle 파일에 다음과 같은 코드를 추가함으로써 해결할 수 있다.
testOptions.unitTests {
includeAndroidResources = true
// ...
}
android 블록안에 위 코드를 작성하도록 하자. 이제
2.9. LiveData를 향한 Assertion 작성하기
이 예제에서 LiveData
를 어떻게 assert 하는지 알아보자
LiveData 를 테스트하기 위해선 두 가지 작업을 해야한다.
InstantTaskExecutorRule
사용하기LiveData observation
을 보장하기
2.9.1. step 1. InstantTaskExecutorRule 사용하기
InstantTaskExecutorRule
은 JUnit Rule이다. JUnit Rule 이란 테스트 클래스에서 동작 방식을 재정의하거나 쉽게 추가하는 것을 가능하게 하는데, 자세한 것은 위 링크에서 알아오도록 하자.
어쨌든 위 룰을 사용하게 되면, Android component 관련 작업들을 모두 한 스레드에서 실행되게 한다. 그러므로 모든 작업이 synchronous하게 동작하여 테스팅을 원할하게 해준다. 특히 LiveData를 이용한다면 무조건 사용해야 한다.
- 이것을 위해 아래와 같은 의존성을 app수준의 build.gradle 파일에 추가하자.
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
- 이제 TasksViewModelTest.kt 파일을 열어 다음과 같은 코드를 추가하자.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
2.9.2. step 2. LiveDataTestUtil.kt 클래스 추가하기
우리가 LiveData를 사용할 때, 우리는 흔히 이 LiveData를 관찰하는 Activity 나 Fragment를 가지고 있다. LiveData 를 관찰하는 것은 매우 중요하다. 우리는 LiveData가 viewModel 에서 동작하는 것처럼 테스팅 하고 싶다. 이것을 위해서 LifeCycleOwner를 사용해 관찰하는 것이 필요하다.
여기서 문제점이 발생한다. test 클래스에서 LiveData를 관찰할 fragment나 activity는 존재하지 않는다. 다행히 우리는 LifeCycleOwner 가 필요없는 observeForever
함수를 사용할 수 있다! 이 함수는
LifeCycleOnwer를 사용하지 않고 LiveData를 관찰할 수 있지만, 그렇기 때문에 자동으로 구독을 해제해주지 않아 memory leak을 방지하기 위해 수동으로 구독을 해제해야한다! 어쨌든 위 함수를 사용해 코드를 작성해보자.
@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 {
// 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)
}
}
위 코드엔 하나의 LiveData를 위한 보일러 플레이트 코드가 너무 많다! 다음과 같은 과정을 사용해 보일러 플레이트 코드를 지워보자
- LiveDataTestUtil.kt 파일을 test source set에 생성하자.
- 아래 코드를 복붙하자
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
@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
}
매우 복잡한 메소드다. 위 함수는 LiveData
2.9.3. step 3. getOrAwait 확장 함수를 사용해 assertion 하기
이제 우리는 위 확장함수를 이용해서 테스팅을 진행할 수 있다.
- LiveData value 를 getOrAwaitValue 를 사용해 얻어오자
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
- value 가 null 이 아닌지 확인하자!
assertThat(value.getContentIfNotHandled(), (not(nullValue())))
최종적인 코드는 다음과 같다.
@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()
// Create observer - no need for it to do anything!
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
assertThat(value.getContentIfNotHandled(), not(nullValue()))
}
}
2.10. 다양한 ViewModel Test 작성해보기
이제 위에서 배운 것들을 활용해 test 코드를 작성해보자. setFilterAllTasks_tasksAddViewVisible()
테스트 함수를 작성해야 한다. 이 테스트 함수는 filter type이 모든 태스크를 보여주도록 설정되었는지 확인하는 테스트이다. 그래서 Add task 버튼이 visible 한지 검사해야 한다.
addNewTask_setsNewTaskEvent()
을 참고해서 TasksViewModel에setFilterAllTasks_tasksAddViewVisible()
함수를 작성해보자. 이 테스트 코드는 filtering mode 를 ALL_TASKS(열거형) 으로 변경하고,tasksAddViewVisible LiveData
를 true인지 확인해야한다.
2.10.1. step 1. 테스트 코드 작성하기
아래 코드를 사용해 시작해보자!
@Test
fun setFilterAllTasks_tasksAddViewVisible() {
// Given a fresh ViewModel
// When the filter type is ALL_TASKS
// Then the "Add task" action is visible
}
참고사항
- ALL_TASKS 는 TasksFilterType 열거형의 요소다.
- add a task 버튼의 visibility는 tasksAddViewVisible LiveData에 의해 결정된다.
2.10.2. step 2. solution 코드와 비교하기
solution code
@Test fun setFilterAllTasks_tasksAddViewVisible() { // Given a fresh ViewModel val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) // When the filter type is ALL_TASKS tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS) val value = tasksViewModel.tasksAddViewVisible.getOrAwaitValue() assertEquals(value, true) // Then the "Add task" action is visible }
2.10.3. step 3. @Before rule 추가하기
이제 지금까지 TasksViewModel 에서 두 개의 테스트를 작성했다! 우리는 이 두 가지 테스트 모두에서 TasksViewModel 을 새로 생성했다. 그러나 이런 설정 코드를 여러 테스트에서 사용한다면, 반복해서 작성하는 대신 @Before
어노테이션을 사용해 반복되는 코드를 삭제할 수 있다!
모든 테스트에서 TasksViewModel을 필요로 하기 때문에 테스트 코드에서 TasksViewModel 생성 부분을 삭제하고, 이것을 다음과 같이 생성해보자!
private lateinit var tasksViewModel: TasksViewModel
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
이제 코드를 실행해보자. 정상적으로 모든 테스트가 통과되는 것을 확인할 수 있다!
'안드로이드 > Android' 카테고리의 다른 글
TDD - 안드로이드 예제와 함께 TDD 를 배워보자! - 2편 (작성 중...) (0) | 2022.01.09 |
---|
Comment