이 Codelab은 Kotlin 기반 Android 고급 교육 과정의 일부입니다. Codelab을 순서대로 진행하는 경우 학습 효과를 극대화할 수 있지만 순서를 바꿔 진행해도 괜찮습니다. 모든 교육 과정 Codelab은 Kotlin 기반 고급 Android Codelab 방문 페이지에 나열되어 있습니다.
소개
두 번째 테스트 Codelab에서는 테스트 더블에 대해 알아봅니다. Android에서 테스트 더블을 사용하는 시점과 종속 항목 삽입, 서비스 로케이터 패턴, 라이브러리를 사용하여 테스트 더블을 구현하는 방법을 알아봅니다. 이 과정에서 다음을 작성하는 방법을 배우게 됩니다.
- 저장소 단위 테스트
- 프래그먼트 및 뷰 모델 통합 테스트
- 프래그먼트 탐색 테스트
기본 요건
다음을 잘 알고 있어야 합니다.
- Kotlin 프로그래밍 언어
- 첫 번째 Codelab에서 다루는 테스트 개념: JUnit, Hamcrest, AndroidX 테스트, Robolectric을 사용하여 Android에서 단위 테스트 작성 및 실행, LiveData 테스트
- 다음 핵심 Android Jetpack 라이브러리:
ViewModel,LiveData, 탐색 구성요소 - 앱 아키텍처 가이드 및 Android 기본사항 Codelab의 패턴을 따르는 애플리케이션 아키텍처
- Android의 코루틴 기본사항
학습할 내용
- 테스트 전략을 계획하는 방법
- 테스트 더블(페이크 및 모의 객체)을 만들고 사용하는 방법
- Android에서 단위 및 통합 테스트를 위해 수동 종속 항목 삽입을 사용하는 방법
- 서비스 로케이터 패턴을 적용하는 방법
- 저장소, 프래그먼트, 뷰 모델, 탐색 구성요소를 테스트하는 방법
다음 라이브러리와 코드 개념을 사용합니다.
실습할 내용
- 테스트 더블 및 종속 항목 삽입을 사용하여 저장소의 단위 테스트를 작성합니다.
- 테스트 더블 및 종속 항목 삽입을 사용하여 뷰 모델의 단위 테스트를 작성합니다.
- Espresso UI 테스트 프레임워크를 사용하여 프래그먼트와 해당 뷰 모델의 통합 테스트를 작성합니다.
- Mockito 및 Espresso를 사용하여 탐색 테스트를 작성합니다.
이 Codelab 시리즈에서는 TO-DO Notes 앱을 사용합니다. 이 앱을 사용하면 완료할 작업을 적어 목록에 표시할 수 있습니다. 그런 다음 완료됨 또는 미완료로 표시하거나, 필터링하거나, 삭제할 수 있습니다.

이 앱은 Kotlin으로 작성되었으며, 화면이 몇 개 있고, Jetpack 구성요소를 사용하며, 앱 아키텍처 가이드의 아키텍처를 따릅니다. 이 앱을 테스트하는 방법을 배우면 동일한 라이브러리와 아키텍처를 사용하는 앱을 테스트할 수 있습니다.
코드 다운로드
시작하려면 코드를 다운로드합니다.
또는 코드에 관한 GitHub 저장소를 클론해도 됩니다.
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
아래 안내에 따라 코드를 숙지하세요.
1단계: 샘플 앱 실행
할 일 앱을 다운로드한 후 Android 스튜디오에서 열고 실행합니다. 컴파일되어야 합니다. 다음을 실행하여 앱을 탐색합니다.
- 플러스 플로팅 액션 버튼으로 새 작업을 만듭니다. 먼저 제목을 입력한 다음 할 일에 관한 추가 정보를 입력합니다. 녹색 체크 FAB로 저장합니다.
- 작업 목록에서 방금 완료한 작업의 제목을 클릭하고 해당 작업의 세부정보 화면을 확인하여 나머지 설명을 확인합니다.
- 목록 또는 세부정보 화면에서 해당 작업의 체크박스를 선택하여 상태를 완료됨으로 설정합니다.
- 작업 화면으로 돌아가 필터 메뉴를 열고 활성 및 완료됨 상태별로 작업을 필터링합니다.
- 탐색 창을 열고 통계를 클릭합니다.
- 개요 화면으로 돌아가 탐색 메뉴에서 완료된 항목 삭제를 선택하여 상태가 완료됨인 모든 작업을 삭제합니다.
2단계: 샘플 앱 코드 살펴보기
할 일 앱은 인기 있는 Architecture Blueprints 테스트 및 아키텍처 샘플 (샘플의 반응형 아키텍처 버전 사용)을 기반으로 합니다. 앱은 앱 아키텍처 가이드의 아키텍처를 따릅니다. 프래그먼트, 저장소, Room과 함께 ViewModel을 사용합니다. 아래 예 중 하나에 익숙하다면 이 앱의 아키텍처가 유사합니다.
- Room with a View Codelab
- Android Kotlin 기초 교육 Codelab
- 고급 Android 교육 Codelab
- Android Sunflower 샘플
- Kotlin을 사용하여 Android 앱 개발 Udacity 교육 과정
특정 레이어의 로직을 깊이 이해하는 것보다 앱의 일반적인 아키텍처를 이해하는 것이 더 중요합니다.
다음은 확인할 수 있는 패키지 요약입니다.
패키지: | |
| 할 일 추가 또는 수정 화면: 할 일을 추가하거나 수정하는 UI 레이어 코드입니다. |
| 데이터 영역: 작업의 데이터 영역을 처리합니다. 여기에는 데이터베이스, 네트워크, 저장소 코드가 포함됩니다. |
| 통계 화면: 통계 화면의 UI 레이어 코드입니다. |
| 작업 세부정보 화면: 단일 작업의 UI 레이어 코드입니다. |
| 작업 화면: 모든 작업 목록의 UI 레이어 코드입니다. |
| 유틸리티 클래스: 앱의 다양한 부분에서 사용되는 공유 클래스입니다(예: 여러 화면에서 사용되는 스와이프 새로고침 레이아웃). |
데이터 레이어 (.data)
이 앱에는 remote 패키지의 시뮬레이션된 네트워킹 레이어와 local 패키지의 데이터베이스 레이어가 포함되어 있습니다. 간단하게 하기 위해 이 프로젝트에서는 실제 네트워크 요청을 만드는 대신 지연이 있는 HashMap만으로 네트워킹 레이어를 시뮬레이션합니다.
DefaultTasksRepository는 네트워킹 레이어와 데이터베이스 레이어 간에 조정하거나 중재하며 UI 레이어에 데이터를 반환합니다.
UI 레이어 ( .addedittask, .statistics, .taskdetail, .tasks)
각 UI 레이어 패키지에는 프래그먼트와 뷰 모델, UI에 필요한 기타 클래스 (예: 작업 목록의 어댑터)가 포함되어 있습니다. TaskActivity는 모든 프래그먼트를 포함하는 활동입니다.
탐색
앱의 탐색은 탐색 구성요소에 의해 제어됩니다. 이 쿼리는 nav_graph.xml 파일에서 정의됩니다. 탐색은 Event 클래스를 사용하여 뷰 모델에서 트리거되며, 뷰 모델은 전달할 인수를 결정합니다. 프래그먼트는 Event를 관찰하고 화면 간 실제 탐색을 실행합니다.
이 Codelab에서는 테스트 더블 및 종속 항목 삽입을 사용하여 저장소, 뷰 모델, 프래그먼트를 테스트하는 방법을 알아봅니다. 이러한 테스트가 무엇인지 알아보기 전에 이러한 테스트를 작성하는 방법과 내용을 안내하는 이유를 이해하는 것이 중요합니다.
이 섹션에서는 Android에 적용되는 일반적인 테스트 권장사항을 다룹니다.
테스트 피라미드
테스트 전략을 고려할 때는 다음과 같은 세 가지 관련 테스트 측면이 있습니다.
- 범위: 테스트가 코드의 얼마나 많은 부분을 다루나요? 테스트는 단일 메서드, 전체 애플리케이션 또는 그 사이에서 실행할 수 있습니다.
- 속도: 테스트가 얼마나 빠르게 실행되나요? 테스트 속도는 밀리초에서 몇 분까지 다양합니다.
- 충실도: 테스트가 얼마나 '실제'에 가까운가요? 예를 들어 테스트하는 코드의 일부에서 네트워크 요청을 해야 하는 경우 테스트 코드에서 실제로 이 네트워크 요청을 수행하나요 아니면 결과를 모의로 처리하나요? 테스트가 실제로 네트워크와 통신하는 경우 충실도가 높다는 의미입니다. 단점은 테스트를 실행하는 데 시간이 오래 걸리거나, 네트워크가 다운되면 오류가 발생하거나, 사용 비용이 많이 들 수 있다는 것입니다.
이러한 측면 사이에는 내재된 절충이 있습니다. 예를 들어 속도와 충실도는 상충 관계에 있습니다. 테스트 속도가 빠를수록 일반적으로 충실도가 낮아지고 그 반대의 경우도 마찬가지입니다. 자동화된 테스트를 나누는 일반적인 방법은 다음 세 가지 카테고리로 나누는 것입니다.
- 단위 테스트: 단일 클래스(일반적으로 해당 클래스의 단일 메서드)에서 실행되는 매우 집중적인 테스트입니다. 단위 테스트가 실패하면 코드의 어느 부분에 문제가 있는지 정확히 알 수 있습니다. 실제 앱에는 하나의 메서드나 클래스 실행보다 훨씬 많은 부분이 포함되므로 충실도가 낮습니다. 코드를 변경할 때마다 실행할 수 있을 만큼 빠릅니다. 이러한 테스트는 대부분 로컬에서 실행되는 테스트 (
test소스 세트)입니다. 예: 뷰 모델 및 저장소에서 단일 메서드 테스트 - 통합 테스트: 여러 클래스의 상호작용을 테스트하여 함께 사용될 때 예상대로 작동하는지 확인합니다. 통합 테스트를 구성하는 한 가지 방법은 작업 저장 기능과 같은 단일 기능을 테스트하는 것입니다. 단위 테스트보다 더 넓은 범위의 코드를 테스트하지만 완전한 충실도보다는 빠른 실행에 최적화되어 있습니다. 상황에 따라 로컬로 실행하거나 계측 테스트로 실행할 수 있습니다. 예: 단일 프래그먼트와 뷰 모델 쌍의 모든 기능을 테스트합니다.
- 엔드 투 엔드 테스트 (E2e): 함께 작동하는 기능의 조합을 테스트합니다. 앱의 상당 부분을 테스트하고 실제 사용량을 긴밀하게 시뮬레이션하므로 일반적으로 느립니다. 통합 테스트는 충실도가 가장 높으며 애플리케이션이 전체적으로 실제로 작동하는지 알려줍니다. 일반적으로 이러한 테스트는 계측 테스트 (
androidTest소스 세트)입니다.
예: 전체 앱을 시작하고 몇 가지 기능을 함께 테스트합니다.
이러한 테스트의 권장 비율은 종종 피라미드로 표시되며, 대부분의 테스트는 단위 테스트입니다.

아키텍처 및 테스트
테스트 피라미드의 모든 수준에서 앱을 테스트할 수 있는 기능은 앱의 아키텍처와 본질적으로 연결되어 있습니다. 예를 들어 매우 잘못 설계된 애플리케이션은 모든 로직을 하나의 메서드 내에 넣을 수 있습니다. 이 경우 앱의 많은 부분을 테스트하는 엔드 투 엔드 테스트를 작성할 수 있지만 단위 테스트나 통합 테스트는 어떻게 작성해야 할까요? 모든 코드가 한곳에 있으므로 단일 단위 또는 기능과 관련된 코드만 테스트하기가 어렵습니다.
더 나은 방법은 애플리케이션 로직을 여러 메서드와 클래스로 나누어 각 부분을 개별적으로 테스트하는 것입니다. 아키텍처는 코드를 나누고 정리하는 방법으로, 단위 테스트와 통합 테스트를 더 쉽게 할 수 있습니다. 테스트할 할 일 앱은 특정 아키텍처를 따릅니다.
이 강의에서는 위의 아키텍처의 일부를 적절히 격리하여 테스트하는 방법을 알아봅니다.
- 먼저 저장소를 단위 테스트합니다.
- 그런 다음 뷰 모델에서 테스트 더블을 사용합니다. 이는 뷰 모델의 단위 테스트와 통합 테스트에 필요합니다.
- 다음으로 프래그먼트와 뷰 모델의 통합 테스트를 작성하는 방법을 알아봅니다.
- 마지막으로 탐색 구성요소를 포함하는 통합 테스트를 작성하는 방법을 알아봅니다.
엔드 투 엔드 테스트는 다음 강의에서 다룹니다.
클래스의 일부 (메서드 또는 소규모 메서드 모음)에 대한 단위 테스트를 작성할 때의 목표는 해당 클래스의 코드만 테스트하는 것입니다.
특정 클래스에서만 코드를 테스트하는 것은 까다로울 수 있습니다. 예를 살펴보겠습니다 main 소스 세트에서 data.source.DefaultTaskRepository 클래스를 엽니다. 이것은 앱의 저장소이며, 다음으로 단위 테스트를 작성할 클래스입니다.
목표는 해당 클래스의 코드만 테스트하는 것입니다. 하지만 DefaultTaskRepository는 작동하기 위해 LocalTaskDataSource, RemoteTaskDataSource과 같은 다른 클래스에 종속됩니다. 다시 말해 LocalTaskDataSource과 RemoteTaskDataSource은 DefaultTaskRepository의 종속 항목입니다.
따라서 DefaultTaskRepository의 모든 메서드는 데이터 소스 클래스의 메서드를 호출하고, 데이터 소스 클래스는 데이터베이스에 정보를 저장하거나 네트워크와 통신하기 위해 다른 클래스의 메서드를 호출합니다.

예를 들어 DefaultTasksRepo에서 이 메서드를 살펴보세요.
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
if (forceUpdate) {
try {
updateTasksFromRemoteDataSource()
} catch (ex: Exception) {
return Result.Error(ex)
}
}
return tasksLocalDataSource.getTasks()
}getTasks은 저장소에 대해 실행할 수 있는 가장 '기본적인' 호출 중 하나입니다. 이 메서드에는 SQLite 데이터베이스에서 읽어오고 네트워크 호출 (updateTasksFromRemoteDataSource 호출)이 포함됩니다. 여기에는 저장소 코드보다 훨씬 많은 코드가 포함됩니다.
저장소를 테스트하기 어려운 구체적인 이유는 다음과 같습니다.
- 이 저장소의 가장 간단한 테스트를 실행하려면 데이터베이스를 만들고 관리하는 방법을 생각해야 합니다. 이로 인해 '로컬 테스트를 사용해야 하나요 아니면 계측 테스트를 사용해야 하나요?'와 같은 질문이 생기고 AndroidX 테스트를 사용하여 시뮬레이션된 Android 환경을 가져와야 하는지 묻게 됩니다.
- 네트워킹 코드와 같은 코드의 일부는 실행하는 데 시간이 오래 걸리거나 때로는 실패하여 오래 실행되고 불안정한 테스트가 생성될 수 있습니다.
- 테스트에서 테스트 실패의 원인이 되는 코드를 진단하는 기능이 손실될 수 있습니다. 테스트에서 저장소 코드가 아닌 코드를 테스트하기 시작할 수 있습니다. 예를 들어 데이터베이스 코드와 같은 종속 코드의 문제로 인해 '저장소' 단위 테스트가 실패할 수 있습니다.
테스트 더블
이 문제를 해결하려면 저장소를 테스트할 때 실제 네트워킹 또는 데이터베이스 코드를 사용하지 말고 테스트 더블을 사용해야 합니다. 테스트 더블은 테스트를 위해 특별히 제작된 클래스 버전입니다. 테스트에서 클래스의 실제 버전을 대체하기 위한 것입니다. 스턴트 더블이 스턴트를 전문으로 하는 배우로 위험한 액션을 할 때 실제 배우를 대체하는 것과 비슷합니다.
다음은 몇 가지 유형의 테스트 더블입니다.
가짜 | 클래스의 '작동' 구현이 있지만 테스트에는 적합하지만 프로덕션에는 적합하지 않은 방식으로 구현된 테스트 더블입니다. |
예시 | 호출된 메서드를 추적하는 테스트 더블입니다. 그런 다음 메서드가 올바르게 호출되었는지에 따라 테스트를 통과하거나 실패합니다. |
스텁 | 로직이 포함되지 않고 프로그래밍된 대로만 반환하는 테스트 더블입니다. 예를 들어 |
더미 | 매개변수로 제공하기만 하면 되는 경우와 같이 전달되지만 사용되지 않는 테스트 더블 |
Spy | 일부 추가 정보도 추적하는 테스트 더블입니다. 예를 들어 |
테스트 더블에 관한 자세한 내용은 화장실에서 하는 테스트: 테스트 더블 알아두기를 참고하세요.
Android에서 가장 일반적으로 사용되는 테스트 더블은 가짜와 모의입니다.
이 작업에서는 실제 데이터 소스에서 분리된 DefaultTasksRepository를 단위 테스트하기 위해 FakeDataSource 테스트 더블을 만듭니다.
1단계: FakeDataSource 클래스 만들기
이 단계에서는 LocalDataSource 및 RemoteDataSource의 테스트 더블이 될 FakeDataSouce라는 클래스를 만듭니다.
- test 소스 세트에서 마우스 오른쪽 버튼을 클릭하고 New -> Package를 선택합니다.

- 내부에 소스 패키지가 있는 데이터 패키지를 만듭니다.
- data/source 패키지에
FakeDataSource라는 새 클래스를 만듭니다.

2단계: TasksDataSource 인터페이스 구현
새 클래스 FakeDataSource을 테스트 더블로 사용하려면 다른 데이터 소스를 대체할 수 있어야 합니다. 이러한 데이터 소스는 TasksLocalDataSource 및 TasksRemoteDataSource입니다.

- 이 두 가지 모두
TasksDataSource인터페이스를 구현하는 방법을 확인하세요.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }FakeDataSource가TasksDataSource를 구현하도록 합니다.
class FakeDataSource : TasksDataSource {
}Android 스튜디오에서 TasksDataSource에 필요한 메서드를 구현하지 않았다고 불만을 제기합니다.
- 빠른 수정 메뉴를 사용하여 Implement members를 선택합니다.

- 모든 방법을 선택하고 확인을 누릅니다.

3단계: FakeDataSource에서 getTasks 메서드 구현
FakeDataSource는 가짜라고 하는 특정 유형의 테스트 더블입니다. 가짜는 클래스의 '작동' 구현이 있는 테스트 더블이지만 테스트에는 적합하지만 프로덕션에는 적합하지 않은 방식으로 구현됩니다. '작동하는' 구현은 클래스가 입력이 주어졌을 때 현실적인 출력을 생성한다는 의미입니다.
예를 들어 가짜 데이터 소스는 네트워크에 연결되지 않으며 데이터베이스에 아무것도 저장하지 않습니다. 대신 메모리 내 목록만 사용합니다. 작업을 가져오거나 저장하는 메서드가 예상 결과를 반환하므로 '예상대로 작동'하지만 서버나 데이터베이스에 저장되지 않으므로 프로덕션에서 이 구현을 사용할 수는 없습니다.
FakeDataSource
- 실제 데이터베이스나 네트워크에 의존하지 않고
DefaultTasksRepository의 코드를 테스트할 수 있습니다. - 테스트를 위한 '실제와 유사한' 구현을 제공합니다.
FakeDataSource생성자를 변경하여 빈 변경 가능한 목록의 기본값이 있는MutableList<Task>?인tasks라는var을 만듭니다.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
데이터베이스 또는 서버 응답인 척하는 작업 목록입니다. 지금은 저장소의 getTasks 메서드를 테스트하는 것이 목표입니다. 이렇게 하면 데이터 소스의 getTasks, deleteAllTasks, saveTask 메서드가 호출됩니다.
이러한 메서드의 가짜 버전을 작성합니다.
getTasks작성:tasks이null이 아니면Success결과를 반환합니다.tasks이null이면Error결과를 반환합니다.- 쓰기
deleteAllTasks: 변경 가능한 작업 목록을 지웁니다. saveTask: 목록에 할 일을 추가합니다.
FakeDataSource에 구현된 메서드는 아래 코드와 같습니다.
override suspend fun getTasks(): Result<List<Task>> {
tasks?.let { return Success(ArrayList(it)) }
return Error(
Exception("Tasks not found")
)
}
override suspend fun deleteAllTasks() {
tasks?.clear()
}
override suspend fun saveTask(task: Task) {
tasks?.add(task)
}필요한 경우 가져오기 문은 다음과 같습니다.
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task이는 실제 로컬 및 원격 데이터 소스가 작동하는 방식과 유사합니다.
이 단계에서는 방금 만든 가짜 테스트 더블을 사용할 수 있도록 수동 종속 항목 삽입이라는 기법을 사용합니다.
주요 문제는 FakeDataSource가 있지만 테스트에서 이를 어떻게 사용하는지 명확하지 않다는 것입니다. 테스트에서만 TasksRemoteDataSource 및 TasksLocalDataSource를 대체해야 합니다. TasksRemoteDataSource와 TasksLocalDataSource는 모두 DefaultTasksRepository의 종속 항목입니다. 즉, DefaultTasksRepositories가 실행되려면 이러한 클래스가 필요하거나 이러한 클래스에 '종속'됩니다.
현재 종속 항목은 DefaultTasksRepository의 init 메서드 내에서 생성됩니다.
DefaultTasksRepository.kt
class DefaultTasksRepository private constructor(application: Application) {
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
// Some other code
init {
val database = Room.databaseBuilder(application.applicationContext,
ToDoDatabase::class.java, "Tasks.db")
.build()
tasksRemoteDataSource = TasksRemoteDataSource
tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
}
// Rest of class
}DefaultTasksRepository 내에서 taskLocalDataSource과 tasksRemoteDataSource을 만들고 할당하므로 기본적으로 하드 코딩됩니다. 테스트 더블을 스왑할 방법이 없습니다.
대신 이러한 데이터 소스를 하드 코딩하는 대신 클래스에 제공해야 합니다. 종속 항목을 제공하는 것을 종속 항목 삽입이라고 합니다. 종속 항목을 제공하는 방법이 다양하므로 종속 항목 삽입 유형도 다양합니다.
생성자 종속 항목 삽입을 사용하면 생성자에 테스트 더블을 전달하여 테스트 더블을 스왑할 수 있습니다.
삽입 없음
| 주입
|
1단계: DefaultTasksRepository에서 생성자 종속 항목 삽입 사용
DefaultTaskRepository의 생성자를Application을 가져오는 것에서 데이터 소스와 코루틴 디스패처를 모두 가져오는 것으로 변경합니다 (테스트를 위해 스왑해야 함. 코루틴에 관한 세 번째 강의 섹션에서 자세히 설명함).
DefaultTasksRepository.kt
// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }
// WITH
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }- 종속 항목을 전달했으므로
init메서드를 삭제합니다. 더 이상 종속 항목을 만들 필요가 없습니다. - 또한 이전 인스턴스 변수를 삭제합니다. 생성자에서 정의합니다.
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO- 마지막으로 새 생성자를 사용하도록
getRepository메서드를 업데이트합니다.
DefaultTasksRepository.kt
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}이제 생성자 종속 항목 삽입을 사용하고 있습니다.
2단계: 테스트에서 FakeDataSource 사용
이제 코드가 생성자 종속 항목 삽입을 사용하므로 가짜 데이터 소스를 사용하여 DefaultTasksRepository를 테스트할 수 있습니다.
DefaultTasksRepository클래스 이름을 마우스 오른쪽 버튼으로 클릭하고 생성, 테스트를 선택합니다.- 메시지에 따라 test 소스 세트에
DefaultTasksRepositoryTest를 만듭니다. - 새
DefaultTasksRepositoryTest클래스 상단에 아래의 멤버 변수를 추가하여 가짜 데이터 소스의 데이터를 나타냅니다.
DefaultTasksRepositoryTest.kt
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }FakeDataSource멤버 변수 2개 (저장소의 각 데이터 소스에 하나씩)와 테스트할DefaultTasksRepository변수 등 세 개의 변수를 만듭니다.
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository테스트 가능한 DefaultTasksRepository를 설정하고 초기화하는 메서드를 만듭니다. 이 DefaultTasksRepository는 테스트 더블 FakeDataSource를 사용합니다.
createRepository이라는 메서드를 만들고@Before주석을 추가합니다.remoteTasks및localTasks목록을 사용하여 가짜 데이터 소스를 인스턴스화합니다.- 방금 만든 두 개의 가짜 데이터 소스와
Dispatchers.Unconfined을 사용하여tasksRepository을 인스턴스화합니다.
최종 메서드는 아래 코드와 같이 표시됩니다.
DefaultTasksRepositoryTest.kt
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}3단계: DefaultTasksRepository getTasks() 테스트 작성
이제 DefaultTasksRepository 테스트를 작성할 시간입니다.
- 저장소의
getTasks메서드 테스트를 작성합니다.true로getTasks를 호출할 때(즉, 원격 데이터 소스에서 다시 로드해야 함) 로컬 데이터 소스가 아닌 원격 데이터 소스에서 데이터를 반환하는지 확인합니다.
DefaultTasksRepositoryTest.kt
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource(){
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}getTasks:를 호출하면 오류가 발생합니다
4단계: runBlockingTest 추가
getTasks은 suspend 함수이므로 이를 호출하려면 코루틴을 실행해야 하므로 코루틴 오류가 예상됩니다. 이를 위해서는 코루틴 범위가 필요합니다. 이 오류를 해결하려면 테스트에서 코루틴 실행을 처리하기 위한 gradle 종속 항목을 추가해야 합니다.
testImplementation을 사용하여 코루틴 테스트에 필요한 종속 항목을 테스트 소스 세트에 추가합니다.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"동기화하는 것을 잊지 마세요.
kotlinx-coroutines-test는 코루틴 테스트 라이브러리로, 코루틴 테스트를 위해 특별히 설계되었습니다. 테스트를 실행하려면 runBlockingTest 함수를 사용하세요. 이는 코루틴 테스트 라이브러리에서 제공하는 함수입니다. 코드 블록을 가져온 다음 동기식으로 즉시 실행되는 특수 코루틴 컨텍스트에서 이 코드 블록을 실행합니다. 즉, 작업이 결정적 순서로 발생합니다. 이렇게 하면 코루틴이 코루틴이 아닌 것처럼 실행되므로 코드 테스트에 적합합니다.
suspend 함수를 호출할 때는 테스트 클래스에서 runBlockingTest를 사용하세요. runBlockingTest 작동 방식과 코루틴 테스트 방법은 이 시리즈의 다음 Codelab에서 자세히 알아봅니다.
- 클래스 위에
@ExperimentalCoroutinesApi을 추가합니다. 이는 클래스에서 실험용 코루틴 API (runBlockingTest)를 사용하고 있음을 알고 있음을 나타냅니다. 그렇지 않으면 경고가 표시됩니다. DefaultTasksRepositoryTest로 돌아가서 전체 테스트를 코드 '블록'으로 가져오도록runBlockingTest를 추가합니다.
이 최종 테스트는 아래 코드와 같습니다.
DefaultTasksRepositoryTest.kt
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
}- 새
getTasks_requestsAllTasksFromRemoteDataSource테스트를 실행하고 작동하는지, 오류가 사라졌는지 확인합니다.
지금까지 저장소를 단위 테스트하는 방법을 살펴보았습니다. 다음 단계에서는 다시 종속 항목 삽입을 사용하여 또 다른 테스트 더블을 만듭니다. 이번에는 뷰 모델의 단위 테스트와 통합 테스트를 작성하는 방법을 보여줍니다.
단위 테스트는 관심 있는 클래스나 메서드만 테스트해야 합니다. 이를 격리된 상태에서 테스트한다고 하며, 여기서 '단위'를 명확하게 격리하고 해당 단위에 속한 코드만 테스트합니다.
따라서 TasksViewModelTest는 TasksViewModel 코드만 테스트해야 하며 데이터베이스, 네트워크 또는 저장소 클래스에서 테스트해서는 안 됩니다. 따라서 뷰 모델의 경우 저장소에서 방금 한 것처럼 가짜 저장소를 만들고 종속 항목 삽입을 적용하여 테스트에서 사용합니다.
이 작업에서는 종속 항목 삽입을 뷰 모델에 적용합니다.

1단계: TasksRepository 인터페이스 만들기
생성자 종속 항목 삽입을 사용하기 위한 첫 번째 단계는 가짜 클래스와 실제 클래스 간에 공유되는 공통 인터페이스를 만드는 것입니다.
실제로는 어떻게 표시되나요? TasksRemoteDataSource, TasksLocalDataSource, FakeDataSource를 살펴보면 모두 동일한 인터페이스인 TasksDataSource를 공유합니다. 이렇게 하면 DefaultTasksRepository의 생성자에서 TasksDataSource를 가져온다고 말할 수 있습니다.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {이를 통해 FakeDataSource을 교체할 수 있습니다.
다음으로 데이터 소스에서와 마찬가지로 DefaultTasksRepository의 인터페이스를 만듭니다. DefaultTasksRepository의 모든 공개 메서드 (공개 API 노출 영역)를 포함해야 합니다.
DefaultTasksRepository을 열고 클래스 이름을 마우스 오른쪽 버튼으로 클릭합니다. 그런 다음 Refactor -> Extract -> Interface를 선택합니다.

- 별도의 파일로 추출을 선택합니다.

- Extract Interface 창에서 인터페이스 이름을
TasksRepository로 변경합니다. - 인터페이스를 형성할 구성원 섹션에서 두 개의 컴패니언 구성원과 비공개 메서드를 제외한 모든 구성원을 선택합니다.

- Refactor을 클릭합니다. 새
TasksRepository인터페이스가 data/source 패키지에 표시됩니다.

이제 DefaultTasksRepository이 TasksRepository을 구현합니다.
- 앱 (테스트 아님)을 실행하여 모든 것이 여전히 작동하는지 확인합니다.
2단계: FakeTestRepository 만들기
이제 인터페이스가 있으므로 DefaultTaskRepository 테스트 더블을 만들 수 있습니다.
- test 소스 세트의 data/source에서 Kotlin 파일과 클래스
FakeTestRepository.kt를 만들고TasksRepository인터페이스에서 확장합니다.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}인터페이스 메서드를 구현해야 한다는 메시지가 표시됩니다.
- 제안 메뉴가 표시될 때까지 오류 위로 마우스를 가져간 다음 Implement members를 클릭하고 선택합니다.
- 모든 방법을 선택하고 확인을 누릅니다.

3단계: FakeTestRepository 메서드 구현
이제 '구현되지 않음' 메서드가 있는 FakeTestRepository 클래스가 있습니다. FakeDataSource를 구현한 방식과 마찬가지로 FakeTestRepository는 로컬 데이터 소스와 원격 데이터 소스 간의 복잡한 중재를 처리하는 대신 데이터 구조로 지원됩니다.
FakeTestRepository는 FakeDataSource 등을 사용할 필요가 없습니다. 입력이 주어지면 현실적인 가짜 출력을 반환하기만 하면 됩니다. LinkedHashMap를 사용하여 작업 목록을 저장하고 MutableLiveData를 사용하여 관찰 가능한 작업을 저장합니다.
FakeTestRepository에서 현재 할 일 목록을 나타내는LinkedHashMap변수와 관찰 가능한 할 일의MutableLiveData를 모두 추가합니다.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}다음 메서드를 구현합니다.
getTasks- 이 메서드는tasksServiceData를 가져와tasksServiceData.values.toList()를 사용하여 목록으로 변환한 후Success결과로 반환해야 합니다.refreshTasks:getTasks()에서 반환된 값으로observableTasks의 값을 업데이트합니다.observeTasks:runBlocking를 사용하여 코루틴을 만들고refreshTasks를 실행한 다음observableTasks를 반환합니다.
아래는 이러한 메서드의 코드입니다.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
// Rest of class
}4단계: 테스트를 위해 addTasks 메서드 추가
테스트할 때는 저장소에 Tasks가 이미 있는 것이 좋습니다. saveTask를 여러 번 호출할 수 있지만 이를 더 쉽게 하기 위해 작업을 추가할 수 있는 테스트 전용 도우미 메서드를 추가합니다.
- 작업의
vararg을 가져와 각 작업을HashMap에 추가한 다음 작업을 새로고침하는addTasks메서드를 추가합니다.
FakeTestRepository.kt
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}이제 몇 가지 주요 메서드가 구현된 테스트용 가짜 저장소가 있습니다. 그런 다음 테스트에서 이를 사용하세요.
이 작업에서는 ViewModel 내에서 가짜 클래스를 사용합니다. 생성자 종속 항목 삽입을 사용하여 TasksViewModel의 생성자에 TasksRepository 변수를 추가하여 생성자 종속 항목 삽입을 통해 두 데이터 소스를 가져옵니다.
뷰 모델은 직접 생성하지 않으므로 이 프로세스가 약간 다릅니다. 예를 들면 다음과 같습니다.
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
위 코드에서와 같이 뷰 모델을 만드는 viewModel's 속성 위임을 사용합니다. 뷰 모델이 생성되는 방식을 변경하려면 ViewModelProvider.Factory를 추가하고 사용해야 합니다. ViewModelProvider.Factory에 익숙하지 않다면 여기에서 자세히 알아보세요.
1단계: TasksViewModel에서 ViewModelFactory 만들기 및 사용
Tasks 화면과 관련된 클래스와 테스트를 업데이트하는 것으로 시작합니다.
- 열기
TasksViewModel를 탭합니다. - 클래스 내에서 생성하는 대신
TasksRepository을 사용하도록TasksViewModel생성자를 변경합니다.
TasksViewModel.kt
// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() {
// Rest of class
}생성자를 변경했으므로 이제 팩토리를 사용하여 TasksViewModel를 생성해야 합니다. 팩토리 클래스를 TasksViewModel과 동일한 파일에 넣어도 되지만 자체 파일에 넣어도 됩니다.
TasksViewModel파일 하단에 클래스 외부에서 일반TasksRepository을 사용하는TasksViewModelFactory을 추가합니다.
TasksViewModel.kt
@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TasksViewModel(tasksRepository) as T)
}
이는 ViewModel가 구성되는 방식을 변경하는 표준 방법입니다. 이제 팩토리가 있으므로 뷰 모델을 생성하는 곳에서 팩토리를 사용합니다.
- 팩토리를 사용하도록
TasksFragment업데이트
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- 앱 코드를 실행하고 모든 것이 여전히 작동하는지 확인합니다.
2단계: TasksViewModelTest 내에서 FakeTestRepository 사용
이제 뷰 모델 테스트에서 실제 저장소를 사용하는 대신 가짜 저장소를 사용할 수 있습니다.
TasksViewModelTest을 열어 줍니다.TasksViewModelTest에FakeTestRepository속성을 추가합니다.
TaskViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeTestRepository
// Rest of class
}setupViewModel메서드를 업데이트하여 태스크가 3개인FakeTestRepository를 만든 다음 이 저장소로tasksViewModel를 생성합니다.
TasksViewModelTest.kt
@Before
fun setupViewModel() {
// We initialise the tasks to 3, with one active and two completed
tasksRepository = FakeTestRepository()
val task1 = Task("Title1", "Description1")
val task2 = Task("Title2", "Description2", true)
val task3 = Task("Title3", "Description3", true)
tasksRepository.addTasks(task1, task2, task3)
tasksViewModel = TasksViewModel(tasksRepository)
}- 더 이상 AndroidX Test
ApplicationProvider.getApplicationContext코드를 사용하지 않으므로@RunWith(AndroidJUnit4::class)주석도 삭제할 수 있습니다. - 테스트를 실행하여 모든 테스트가 여전히 작동하는지 확인합니다.
이제 생성자 종속 항목 삽입을 사용하여 DefaultTasksRepository를 종속 항목으로 삭제하고 테스트에서 FakeTestRepository로 대체했습니다.
3단계: TaskDetail 프래그먼트 및 ViewModel도 업데이트
TaskDetailFragment 및 TaskDetailViewModel에 동일한 변경사항을 적용합니다. 이렇게 하면 다음에 TaskDetail 테스트를 작성할 때 코드가 준비됩니다.
- 열기
TaskDetailViewModel를 탭합니다. - 생성자를 업데이트합니다.
TaskDetailViewModel.kt
// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }TaskDetailViewModel파일 하단에 클래스 외부에서TaskDetailViewModelFactory를 추가합니다.
TaskDetailViewModel.kt
@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TaskDetailViewModel(tasksRepository) as T)
}- 팩토리를 사용하도록
TasksFragment업데이트
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- 코드를 실행하여 모든 것이 제대로 작동하는지 확인합니다.
이제 TasksFragment 및 TasksDetailFragment에서 실제 저장소 대신 FakeTestRepository를 사용할 수 있습니다.
다음으로 통합 테스트를 작성하여 프래그먼트와 뷰 모델 상호작용을 테스트합니다. 뷰 모델 코드가 UI를 적절하게 업데이트하는지 확인할 수 있습니다. 이렇게 하려면 다음을 사용합니다.
- ServiceLocator 패턴
- Espresso 및 Mockito 라이브러리
통합 테스트 는 여러 클래스의 상호작용을 테스트하여 함께 사용될 때 예상대로 작동하는지 확인합니다. 이러한 테스트는 로컬 (test 소스 세트) 또는 계측 테스트 (androidTest 소스 세트)로 실행할 수 있습니다.

이 경우 각 프래그먼트를 가져와 프래그먼트와 뷰 모델의 통합 테스트를 작성하여 프래그먼트의 주요 기능을 테스트합니다.
1단계: Gradle 종속 항목 추가
- 다음 Gradle 종속 항목을 추가합니다.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Testing code should not be included in the main code.
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
이러한 종속 항목에는 다음이 포함됩니다.
junit:junit: 기본 테스트 문을 작성하는 데 필요한 JUnit입니다.androidx.test:core- 핵심 AndroidX 테스트 라이브러리kotlinx-coroutines-test- 코루틴 테스트 라이브러리androidx.fragment:fragment-testing: 테스트에서 프래그먼트를 만들고 상태를 변경하는 AndroidX 테스트 라이브러리
androidTest 소스 세트에서 이러한 라이브러리를 사용하므로 androidTestImplementation를 사용하여 종속 항목으로 추가합니다.
2단계: TaskDetailFragmentTest 클래스 만들기
TaskDetailFragment에는 단일 작업에 대한 정보가 표시됩니다.

다른 프래그먼트에 비해 기능이 상당히 기본적인 TaskDetailFragment의 프래그먼트 테스트를 작성하는 것으로 시작합니다.
- 열기
taskdetail.TaskDetailFragment를 탭합니다. - 이전과 마찬가지로
TaskDetailFragment의 테스트를 생성합니다. 기본 선택사항을 수락하고 androidTest 소스 세트 (test소스 세트 아님)에 넣습니다.

TaskDetailFragmentTest클래스에 다음 주석을 추가합니다.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}이러한 주석의 목적은 다음과 같습니다.
@MediumTest: 테스트를 '중간 런타임' 통합 테스트로 표시합니다 (@SmallTest단위 테스트 및@LargeTest대규모 엔드 투 엔드 테스트와 비교). 이를 통해 실행할 테스트 크기를 그룹화하고 선택할 수 있습니다.@RunWith(AndroidJUnit4::class): AndroidX 테스트를 사용하는 모든 클래스에서 사용됩니다.
3단계: 테스트에서 프래그먼트 실행
이 작업에서는 AndroidX Testing 라이브러리를 사용하여 TaskDetailFragment를 실행합니다. FragmentScenario은 프래그먼트를 래핑하고 테스트를 위해 프래그먼트의 수명 주기를 직접 제어할 수 있도록 하는 AndroidX 테스트의 클래스입니다. 프래그먼트 테스트를 작성하려면 테스트 중인 프래그먼트 (TaskDetailFragment)의 FragmentScenario를 만듭니다.
- 이 테스트를
TaskDetailFragmentTest에 복사합니다.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() {
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
위 코드는 다음을 수행합니다.
- 작업을 만듭니다.
- 프래그먼트에 전달되는 작업의 프래그먼트 인수를 나타내는
Bundle를 만듭니다. launchFragmentInContainer함수는 이 번들과 테마를 사용하여FragmentScenario를 만듭니다.
아무것도 어설션하지 않으므로 아직 완료된 테스트가 아닙니다. 지금은 테스트를 실행하고 어떤 일이 일어나는지 관찰하세요.
- 계측 테스트이므로 에뮬레이터 또는 기기가 표시되는지 확인하세요.
- 테스트를 실행합니다.
몇 가지 사항이 발생합니다.
- 첫째, 계측 테스트이므로 테스트는 실제 기기 (연결된 경우) 또는 에뮬레이터에서 실행됩니다.
- 프래그먼트가 실행됩니다.
- 다른 프래그먼트를 통해 탐색하지 않고 활동과 연결된 메뉴가 없습니다. 단지 프래그먼트일 뿐입니다.
마지막으로 자세히 살펴보면 작업 데이터를 로드하지 못해 프래그먼트에 '데이터 없음'이라고 표시됩니다.

테스트에서는 TaskDetailFragment를 로드해야 하며 (이미 완료함) 데이터가 올바르게 로드되었는지 어설션해야 합니다. 데이터가 없는 이유는 무엇인가요? 작업을 만들었지만 저장소에 저장하지 않았기 때문입니다.
@Test
fun activeTaskDetails_DisplayedInUi() {
// This DOES NOT save the task anywhere
val activeTask = Task("Active Task", "AndroidX Rocks", false)
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
이 FakeTestRepository가 있지만 프래그먼트의 실제 저장소를 가짜 저장소로 대체할 방법이 필요합니다. 다음 단계에서 이 작업을 수행합니다.
이 작업에서는 ServiceLocator를 사용하여 가짜 저장소를 프래그먼트에 제공합니다. 이렇게 하면 프래그먼트 및 뷰 모델 통합 테스트를 작성할 수 있습니다.
뷰 모델이나 저장소에 종속 항목을 제공해야 했던 이전과 달리 여기서는 생성자 종속 항목 삽입을 사용할 수 없습니다. 생성자 종속 항목 삽입을 사용하려면 클래스를 생성해야 합니다. 프래그먼트와 활동은 생성하지 않고 일반적으로 생성자에 액세스할 수 없는 클래스의 예입니다.
프래그먼트를 구성하지 않으므로 생성자 종속 항목 삽입을 사용하여 저장소 테스트 더블 (FakeTestRepository)을 프래그먼트로 바꿀 수 없습니다. 대신 서비스 로케이터 패턴을 사용하세요. 서비스 로케이터 패턴은 종속 항목 삽입의 대안입니다. 여기에는 일반 코드와 테스트 코드 모두에 종속 항목을 제공하는 목적으로 '서비스 로케이터'라는 싱글톤 클래스를 만드는 작업이 포함됩니다. 일반 앱 코드 (main 소스 세트)에서 이러한 종속 항목은 모두 일반 앱 종속 항목입니다. 테스트를 위해 서비스 로케이터를 수정하여 종속 항목의 테스트 더블 버전을 제공합니다.
서비스 로케이터를 사용하지 않음
| 서비스 로케이터 사용
|
이 Codelab 앱의 경우 다음을 실행합니다.
- 저장소를 구성하고 저장할 수 있는 서비스 로케이터 클래스를 만듭니다. 기본적으로 '일반' 저장소를 구성합니다.
- 저장소가 필요한 경우 서비스 로케이터를 사용하도록 코드를 리팩터링합니다.
- 테스트 클래스에서 '일반' 저장소를 테스트 더블로 바꾸는 서비스 로케이터의 메서드를 호출합니다.
1단계: ServiceLocator 만들기
ServiceLocator 클래스를 만들어 보겠습니다. 기본 애플리케이션 코드에서 사용되므로 나머지 앱 코드와 함께 기본 소스 세트에 있습니다.
참고: ServiceLocator는 싱글톤이므로 클래스에 Kotlin object 키워드를 사용하세요.
- 기본 소스 세트의 최상위 수준에 ServiceLocator.kt 파일을 만듭니다.
ServiceLocator이라는object을 정의합니다.database및repository인스턴스 변수를 만들어 둘 다null로 설정합니다.- 여러 스레드에서 사용할 수 있으므로
@Volatile로 저장소를 주석 처리합니다 (@Volatile은 여기에 자세히 설명되어 있음).
코드는 아래와 같이 표시됩니다.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}현재 ServiceLocator가 해야 하는 일은 TasksRepository를 반환하는 방법을 아는 것뿐입니다. 기존 DefaultTasksRepository를 반환하거나 필요한 경우 새 DefaultTasksRepository를 만들어 반환합니다.
다음 함수를 정의합니다.
provideTasksRepository: 이미 있는 저장소를 제공하거나 새 저장소를 만듭니다. 이 메서드는 여러 스레드가 실행되는 상황에서 실수로 두 개의 저장소 인스턴스를 만드는 것을 방지하기 위해this에서synchronized이어야 합니다.createTasksRepository- 새 저장소를 만드는 코드createTaskLocalDataSource를 호출하고 새TasksRemoteDataSource를 만듭니다.createTaskLocalDataSource- 새 로컬 데이터 소스를 만드는 코드createDataBase에 전화를 겁니다.createDataBase- 새 데이터베이스를 만드는 코드
완성된 코드는 아래와 같습니다.
ServiceLocator.kt
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
fun provideTasksRepository(context: Context): TasksRepository {
synchronized(this) {
return tasksRepository ?: createTasksRepository(context)
}
}
private fun createTasksRepository(context: Context): TasksRepository {
val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
tasksRepository = newRepo
return newRepo
}
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
private fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, "Tasks.db"
).build()
database = result
return result
}
}2단계: 애플리케이션에서 ServiceLocator 사용
테스트가 아닌 기본 애플리케이션 코드를 변경하여 한 곳 (ServiceLocator)에 저장소를 만듭니다.
저장소 클래스의 인스턴스는 하나만 만들어야 합니다. 이를 위해 애플리케이션 클래스에서 서비스 로케이터를 사용합니다.
- 패키지 계층의 최상위 수준에서
TodoApplication를 열고 저장소의val를 만들어ServiceLocator.provideTaskRepository를 사용하여 가져온 저장소를 할당합니다.
TodoApplication.kt
class TodoApplication : Application() {
val taskRepository: TasksRepository
get() = ServiceLocator.provideTasksRepository(this)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
이제 애플리케이션에 저장소를 만들었으므로 DefaultTasksRepository에서 이전 getRepository 메서드를 삭제할 수 있습니다.
DefaultTasksRepository를 열고 컴패니언 객체를 삭제합니다.
DefaultTasksRepository.kt
// DELETE THIS COMPANION OBJECT
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}이제 getRepository을 사용하던 모든 곳에서 애플리케이션의 taskRepository을 대신 사용합니다. 이렇게 하면 저장소를 직접 만드는 대신 ServiceLocator가 제공한 저장소를 가져올 수 있습니다.
TaskDetailFragement을 열고 클래스 상단에서getRepository호출을 찾습니다.- 이 호출을
TodoApplication에서 저장소를 가져오는 호출로 바꿉니다.
TaskDetailFragment.kt
// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}TasksFragment에도 동일한 작업을 실행합니다.
TasksFragment.kt
// REPLACE this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}StatisticsViewModel및AddEditTaskViewModel의 경우 저장소를 획득하는 코드를 업데이트하여TodoApplication의 저장소를 사용합니다.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- 테스트가 아닌 애플리케이션을 실행합니다.
리팩터링만 했으므로 앱이 문제없이 동일하게 실행되어야 합니다.
3단계: Create FakeAndroidTestRepository
테스트 소스 세트에 이미 FakeTestRepository이 있습니다. 기본적으로 test 및 androidTest 소스 세트 간에 테스트 클래스를 공유할 수 없습니다. 따라서 androidTest 소스 세트에서 FakeTestRepository 클래스를 복제하고 FakeAndroidTestRepository라고 호출해야 합니다.
androidTest소스 세트를 마우스 오른쪽 버튼으로 클릭하고 data 패키지를 만듭니다. 다시 마우스 오른쪽 버튼으로 클릭하고 소스 패키지를 만듭니다.- 이 소스 패키지에
FakeAndroidTestRepository.kt라는 새 클래스를 만듭니다. - 다음 코드를 해당 클래스에 복사합니다.
FakeAndroidTestRepository.kt
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap
class FakeAndroidTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private var shouldReturnError = false
private val observableTasks = MutableLiveData<Result<List<Task>>>()
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { refreshTasks() }
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
tasksServiceData[taskId]?.let {
return Success(it)
}
return Error(Exception("Could not find task"))
}
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
return Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
tasksServiceData[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source.
throw NotImplementedError()
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
tasksServiceData[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
throw NotImplementedError()
}
override suspend fun clearCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
refreshTasks()
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
refreshTasks()
}
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
}
4단계: 테스트를 위해 ServiceLocator 준비
이제 테스트할 때 ServiceLocator를 사용하여 테스트 더블을 스왑할 수 있습니다. 이렇게 하려면 ServiceLocator 코드에 코드를 추가해야 합니다.
- 열기
ServiceLocator.kt를 탭합니다. tasksRepository의 setter를@VisibleForTesting로 표시합니다. 이 주석은 setter가 공개된 이유가 테스트 때문임을 표현하는 방법입니다.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set테스트를 단독으로 실행하든 테스트 그룹으로 실행하든 테스트는 정확히 동일하게 실행되어야 합니다. 즉, 테스트가 서로 종속되는 동작이 없어야 합니다 (테스트 간에 객체를 공유하지 않음).
ServiceLocator는 싱글톤이므로 테스트 간에 실수로 공유될 수 있습니다. 이를 방지하려면 테스트 사이에 ServiceLocator 상태를 올바르게 재설정하는 메서드를 만드세요.
Any값이 있는lock이라는 인스턴스 변수를 추가합니다.
ServiceLocator.kt
private val lock = Any()- 데이터베이스를 지우고 저장소와 데이터베이스를 모두 null로 설정하는 테스트 전용 메서드
resetRepository를 추가합니다.
ServiceLocator.kt
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}5단계: ServiceLocator 사용
이 단계에서는 ServiceLocator를 사용합니다.
- 열기
TaskDetailFragmentTest를 탭합니다. lateinit TasksRepository변수를 선언합니다.- 각 테스트 전에
FakeAndroidTestRepository를 설정하고 각 테스트 후에 정리하는 설정 및 해체 메서드를 추가합니다.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
activeTaskDetails_DisplayedInUi()의 함수 본문을runBlockingTest로 래핑합니다.- 프래그먼트를 실행하기 전에 저장소에
activeTask를 저장합니다.
repository.saveTask(activeTask)최종 테스트는 아래 코드와 같습니다.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}- 전체 클래스에
@ExperimentalCoroutinesApi주석을 답니다.
완료되면 코드는 다음과 같이 표시됩니다.
TaskDetailFragmentTest.kt
@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
}
activeTaskDetails_DisplayedInUi()테스트를 실행합니다.
이전과 마찬가지로 프래그먼트가 표시되지만 이번에는 저장소를 올바르게 설정했으므로 작업 정보가 표시됩니다.

이 단계에서는 Espresso UI 테스트 라이브러리를 사용하여 첫 번째 통합 테스트를 완료합니다. UI에 대한 어설션으로 테스트를 추가할 수 있도록 코드를 구조화했습니다. 이를 위해 Espresso 테스트 라이브러리를 사용합니다.
Espresso는 다음 작업을 지원합니다.
- 버튼 클릭, 막대 슬라이드, 화면 아래로 스크롤 등 뷰와 상호작용합니다.
- 특정 뷰가 화면에 있거나 특정 상태 (예: 특정 텍스트 포함, 체크박스가 선택됨 등)에 있는지 어설션합니다.
1단계: Gradle 종속 항목 참고
Android 프로젝트에 기본적으로 포함되어 있으므로 기본 Espresso 종속 항목이 이미 있습니다.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}androidx.test.espresso:espresso-core: 이 핵심 Espresso 종속 항목은 새 Android 프로젝트를 만들 때 기본적으로 포함됩니다. 여기에는 대부분의 뷰와 뷰의 작업에 관한 기본 테스트 코드가 포함되어 있습니다.
2단계: 애니메이션 사용 중지
Espresso 테스트는 실제 기기에서 실행되므로 기본적으로 계측 테스트입니다. 발생할 수 있는 한 가지 문제는 애니메이션입니다. 애니메이션이 지연되고 뷰가 화면에 있는지 테스트하려고 하지만 아직 애니메이션이 진행 중인 경우 Espresso에서 실수로 테스트가 실패할 수 있습니다. 이로 인해 Espresso 테스트가 불안정해질 수 있습니다.
Espresso UI 테스트의 경우 애니메이션을 사용 중지하는 것이 좋습니다 (테스트도 더 빠르게 실행됨).
- 테스트 기기에서 설정 > 개발자 옵션으로 이동합니다.
- 창 애니메이션 배율, 전환 애니메이션 배율, 애니메이터 길이 배율의 세 가지 설정을 사용 중지합니다.

3단계: Espresso 테스트 살펴보기
Espresso 테스트를 작성하기 전에 Espresso 코드를 살펴보세요.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))이 명령문은 ID가 task_detail_complete_checkbox인 체크박스 뷰를 찾아 클릭한 다음 체크되었는지 확인합니다.
대부분의 Espresso 문은 다음 네 부분으로 구성됩니다.
onViewonView은 Espresso 문을 시작하는 정적 Espresso 메서드의 예입니다. onView이 가장 일반적이지만 onData과 같은 다른 옵션도 있습니다.
2. ViewMatcher
withId(R.id.task_detail_title_text)withId는 ID로 뷰를 가져오는 ViewMatcher의 예입니다. 문서에서 확인할 수 있는 다른 뷰 매처도 있습니다.
3. ViewAction
perform(click())ViewAction를 사용하는 perform 메서드 ViewAction는 뷰에 대해 실행할 수 있는 작업입니다. 예를 들어 여기서는 뷰를 클릭하는 것입니다.
check(matches(isChecked()))ViewAssertion를 사용하는 check ViewAssertion는 뷰에 관한 내용을 확인하거나 어설션합니다. 가장 일반적으로 사용하는 ViewAssertion는 matches 어설션입니다. 어설션을 완료하려면 다른 ViewMatcher(이 경우 isChecked)를 사용합니다.

Espresso 문에서 perform와 check를 모두 호출하는 것은 아닙니다. check를 사용하여 어설션을 실행하거나 perform를 사용하여 ViewAction를 실행하는 문이 있을 수 있습니다.
- 열기
TaskDetailFragmentTest.kt를 탭합니다. activeTaskDetails_DisplayedInUi테스트를 업데이트합니다.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}
필요한 경우 가져오기 문은 다음과 같습니다.
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not// THEN주석 뒤에 오는 모든 내용은 Espresso를 사용합니다. 테스트 구조와withId사용을 검토하고 세부정보 페이지의 모양에 관한 어설션을 확인합니다.- 테스트를 실행하고 통과하는지 확인합니다.
4단계: 선택사항, 자체 Espresso 테스트 작성
이제 직접 테스트를 작성해 보세요.
completedTaskDetails_DisplayedInUi라는 새 테스트를 만들고 이 스켈레톤 코드를 복사합니다.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
// WHEN - Details fragment launched to display task
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
}- 이전 테스트를 살펴보고 이 테스트를 완료합니다.
- 실행하여 테스트가 통과하는지 확인합니다.
완성된 completedTaskDetails_DisplayedInUi는 다음 코드와 같습니다.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
repository.saveTask(completedTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
}이 마지막 단계에서는 모의라는 다른 유형의 테스트 더블과 테스트 라이브러리 Mockito를 사용하여 탐색 구성요소를 테스트하는 방법을 알아봅니다.
이 Codelab에서는 테스트 더블인 페이크를 사용했습니다. 가짜는 여러 유형의 테스트 더블 중 하나입니다. 탐색 구성요소를 테스트하는 데 어떤 테스트 더블을 사용해야 하나요?
탐색이 어떻게 이루어지는지 생각해 보세요. TasksFragment에서 태스크 중 하나를 눌러 태스크 세부정보 화면으로 이동한다고 가정해 보겠습니다.

다음은 TasksFragment에서 버튼을 누르면 작업 세부정보 화면으로 이동하는 코드입니다.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
탐색은 navigate 메서드 호출로 인해 발생합니다. 어설션 문을 작성해야 하는 경우 TaskDetailFragment로 이동했는지 테스트하는 간단한 방법이 없습니다. 탐색은 TaskDetailFragment를 초기화하는 것 외에 명확한 출력이나 상태 변경이 발생하지 않는 복잡한 작업입니다.
navigate 메서드가 올바른 작업 매개변수로 호출되었는지 확인할 수 있습니다. 모의 테스트 더블은 특정 메서드가 호출되었는지 확인합니다.
Mockito는 테스트 더블을 만드는 프레임워크입니다. API와 이름에 모의라는 단어가 사용되지만 모의를 만드는 데만 사용되는 것은 아닙니다. 스텁과 스파이도 만들 수 있습니다.
Mockito를 사용하여 navigate 메서드가 올바르게 호출되었는지 어설션할 수 있는 모의 NavigationController를 만듭니다.
1단계: Gradle 종속 항목 추가
- Gradle 종속 항목을 추가합니다.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
org.mockito:mockito-core: Mockito 종속 항목입니다.dexmaker-mockito- Android 프로젝트에서 Mockito를 사용하는 데 필요한 라이브러리입니다. Mockito는 런타임에 클래스를 생성해야 합니다. Android에서는 dex 바이트 코드를 사용하여 이 작업을 실행하므로 이 라이브러리를 사용하면 Mockito가 Android에서 런타임 중에 객체를 생성할 수 있습니다.androidx.test.espresso:espresso-contrib- 이 라이브러리는DatePicker,RecyclerView과 같은 고급 뷰의 테스트 코드가 포함된 외부 기여로 구성됩니다 (이름이 그 이유임). 또한 접근성 검사와 나중에 설명할CountingIdlingResource클래스가 포함되어 있습니다.
2단계: TasksFragmentTest 만들기
TasksFragment를 엽니다.TasksFragment클래스 이름을 마우스 오른쪽 버튼으로 클릭하고 생성, 테스트를 차례로 선택합니다. androidTest 소스 세트에서 테스트를 만듭니다.- 이 코드를
TasksFragmentTest에 복사합니다.
TasksFragmentTest.kt
@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
}이 코드는 작성한 TaskDetailFragmentTest 코드와 비슷합니다. FakeAndroidTestRepository를 설정하고 해체합니다. 할 일 목록에서 할 일을 클릭하면 올바른 TaskDetailFragment로 이동하는지 테스트하는 탐색 테스트를 추가합니다.
clickTask_navigateToDetailFragmentOne테스트를 추가합니다.
TasksFragmentTest.kt
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
}
- Mockito의
mock함수를 사용하여 모의 객체를 만듭니다.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)Mockito에서 모의하려면 모의하려는 클래스를 전달합니다.
다음으로 NavController을 프래그먼트와 연결해야 합니다. onFragment를 사용하면 프래그먼트 자체에서 메서드를 호출할 수 있습니다.
- 새 모의를 프래그먼트의
NavController로 만듭니다.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}- 텍스트가 'TITLE1'인
RecyclerView에서 항목을 클릭하는 코드를 추가합니다.
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))RecyclerViewActions는 espresso-contrib 라이브러리의 일부이며 RecyclerView에서 Espresso 작업을 실행할 수 있습니다.
navigate이 올바른 인수로 호출되었는지 확인합니다.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")Mockito의 verify 메서드는 이를 모의로 만듭니다. 모의 navController가 매개변수 (ID가 'id1'인 actionTasksFragmentToTaskDetailFragment)로 특정 메서드 (navigate)를 호출했는지 확인할 수 있습니다.
전체 테스트는 다음과 같습니다.
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
)
}- 테스트를 실행합니다.
요약하자면 탐색을 테스트하려면 다음을 수행하면 됩니다.
- Mockito를 사용하여
NavController모의 객체를 만듭니다. - 모의
NavController을 프래그먼트에 연결합니다. - navigate가 올바른 작업 및 매개변수로 호출되었는지 확인합니다.
3단계: 선택사항, clickAddTaskButton_navigateToAddEditFragment 작성
탐색 테스트를 직접 작성할 수 있는지 확인하려면 이 작업을 시도해 보세요.
- + FAB을 클릭하면
AddEditTaskFragment로 이동하는지 확인하는 테스트clickAddTaskButton_navigateToAddEditFragment를 작성합니다.
답변은 아래와 같습니다.
TasksFragmentTest.kt
@Test
fun clickAddTaskButton_navigateToAddEditFragment() {
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the "+" button
onView(withId(R.id.add_task_fab)).perform(click())
// THEN - Verify that we navigate to the add screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null, getApplicationContext<Context>().getString(R.string.add_task)
)
)
}여기를 클릭하여 시작한 코드와 최종 코드의 차이를 확인하세요.
완료된 Codelab의 코드를 다운로드하려면 아래의 git 명령어를 사용하면 됩니다.
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_2
또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.
이 Codelab에서는 수동 종속성 삽입 및 서비스 로케이터를 설정하는 방법과 Android Kotlin 앱에서 가짜 및 모의 객체를 사용하는 방법을 다뤘습니다. 특히 다음 항목이 중요합니다.
- 테스트할 내용과 테스트 전략에 따라 앱에 구현할 테스트 종류가 결정됩니다. 단위 테스트는 집중적이고 빠릅니다. 통합 테스트는 프로그램의 여러 부분 간의 상호작용을 확인합니다. 엔드 투 엔드 테스트는 기능을 검증하고 충실도가 가장 높으며 계측되는 경우가 많고 실행하는 데 시간이 더 오래 걸릴 수 있습니다.
- 앱의 아키텍처는 테스트의 난이도에 영향을 미칩니다.
- TDD(테스트 기반 개발)는 먼저 테스트를 작성한 다음 테스트를 통과하는 기능을 만드는 전략입니다.
- 테스트를 위해 앱의 일부를 격리하려면 테스트 더블을 사용하면 됩니다. 테스트 더블은 테스트를 위해 특별히 제작된 클래스 버전입니다. 예를 들어 데이터베이스나 인터넷에서 데이터를 가져오는 것을 모의로 처리합니다.
- 종속 항목 삽입을 사용하여 실제 클래스를 테스트 클래스(예: 저장소 또는 네트워킹 레이어)로 대체합니다.
- 계측 테스트 (
androidTest)를 사용하여 UI 구성요소를 실행합니다. - 생성자 종속 항목 삽입을 사용할 수 없는 경우(예: 프래그먼트를 실행하는 경우) 서비스 로케이터를 사용할 수 있습니다. 서비스 로케이터 패턴은 종속 항목 삽입의 대안입니다. 여기에는 일반 코드와 테스트 코드 모두에 종속 항목을 제공하는 목적으로 '서비스 로케이터'라는 싱글톤 클래스를 만드는 작업이 포함됩니다.
Udacity 과정:
Android 개발자 문서:
- 앱 아키텍처 가이드
runBlocking및runBlockingTestFragmentScenario- Espresso
- Mockito
- JUnit4
- AndroidX 테스트 라이브러리
- AndroidX 아키텍처 구성요소 핵심 테스트 라이브러리
- 소스 세트
- 명령줄에서 테스트
동영상:
기타:
이 과정의 다른 Codelab 링크는 Kotlin 기반 Android 고급 Codelab 방문 페이지를 참고하세요.




