Advanced Android in Kotlin 05.2: Introduction to Test Doubles and Dependency Injection | Android Developers
In this codelab you’ll learn to set up manual dependency injection, a service locator, and how to use fakes and mocks in your Android Kotlin apps. In doing so, you’ll learn how to test a repository and write fragment integration tests.
developer.android.com
안드로이드 독학하기!
테스트 Codelab을 이어서 진행하고 있습니다.😆
6. Fake Repository 만들기
지금까지 FakeDataSource를 이용해 Repository를 테스트해보았습니다.
이번에는 FakeRepository를 이용해 ViewModel를 테스트하는 방법에 대해 공부해보겠습니다.
이전에도 알아보았듯이 Unit test에서는 테스트하고 싶은 클래스 또는 메소드만 테스트해야 합니다.
이를 testing in isolation이라고 합니다.
이번 테스트에서도 이러한 독립성을 지키고 Unit test를 수행하기 위해 실제 Repository가 아닌
Fake Repository를 만들어 의존성 주입으로 View Model에서 사용할 수 있도록 하겠습니다.😌👍🏻
6.1 TasksRepository Interface 만들기
이를 위해서는 먼저 기존의 코드를 수정할 필요가 있습니다.
실제 repository 클래스를 fake repository 클래스로 교체가능하도록 공통적인 interface를 만들어주도록 합시다.
클래스에서 인터페이스를 만드는 방법은 간단합니다.
직접 모든 public method를 가지는 인터페이스를 구현할 수도 있고, 아래와 같이 IDE의 기능을 빌릴 수도 있습니다.
👇🏻 인터페이스 만드는 방법
Extract Interface를 클릭하면, Interface 내에 구현할 메서드를 선택할 수 있는 창이 나옵니다.
이곳에서 private member와 companion member를 제외하고 모두 선택해주겠습니다.🤗
아래 이미지와 같이 TaksRepository 파일이 성공적으로 추가된 것을 확인할 수 있습니다.
🌹 프로젝트 구조를 변경하였을 때는 반드시 앱을 직접 실행해 정상적으로 작동하는지 확인해봐야 합니다!
6.2 FakeTestRepository 만들기
이제 Fake Repository를 만들 준비가 완료되었고, 실제로 DefaultTasksRepository의 Test Double를 만들어보도록 하겠습니다.
test 디렉토리의 date/source 패키지 하위에 FakeTestRepository 파일을 만들고,
TasksRepository 인터페이스를 확장하도록 하면 끝입니다.
👇🏻 요렇게!
class FakeTestRepository: TasksRepository {}
6.3 FakeTestRepository의 메서드 구현하기
🔥 중요! FakeRepository에서는 FakeDataSource가 필요없습니다.
FakeRepository라 해서 이전에 구현해 두었던 FakeDataSource를 사용하는 것이 아니라, 주어진 input에 대해 적절한 가짜 output을 생성해서 반환해주기만 하면 됩니다.
이곳에서는 LinkedHashMap을 사용해 데이터베이스 기능을 대신 제공하여 tasks들의 정보를 저장하고,
MutableLiveData를 이용해 tasks를 observing하도록 하겠습니다.
6.3.1 LinkedHashMap과 MutableLiveData 변수 추가해주기
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
}
6.3.2 실제 구현체(DefaultTasksRepository)를 참고하여 메서드 구현하기
예를 들어, getTasks는 tasks 리스트를 바로 반환할 수 있도록 아래와 같이 구현할 수 있습니다.
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
실제 getTasks 코드는 복잡한데 Test Double의 코드가 너무 간단해 의구심이 들 수 도 있습니다.
하지만 FakeRepository에서는 그 tasks 리스트가 remote에서 온건지 local에서 받아온건지 신경쓰지 않고 그저 tasks 리스트를 반환하는 작업만 동작하면 되므로 위와 같이 간단하게 구현해도 문제는 되지 않습니다.
이와 비슷하게 refreshTasks는
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
이렇게 간단하게 완성할 수도 있습니다.🤨
한 메서드만 더 확인해보도록 하겠습니다. 바로 viewModel에서 observing할 수 있는 LiveData를 반환하는 observeTasks 메서드입니다.
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
여기서 갑자기 runBlocking이 나온 이유가 이해되지 않습니다...😭
CodeLab에서는 실제 구현체(DefaultTasksRepository)와 최대한 동일하게 동작하도록 하기 위해 runBlocking을 추가하였다고 합니다.
여기서 또한 중요하게 볼 것은!
이전 포스트에서는 분명 테스트에서 코루틴을 사용할 경우에는 runTest(이전에는 runBlockingTest)를 사용했었습니다.
왜 이곳에서는 runBlocking인 것일까요?
🌹 runTest(runBlockingTest)는 @Test 어노테이션이 붙은 함수에서, 일반 Fake Test Double에서는 runBlocking을 사용
이라고 합니다.🤗
6.3.3 addTasks 메서드 추가하기
!! 테스트를 해보려니 Repository에 추가되어 있는 데이터가 없어 불편함이 있습니다.
이를 위해 미리 Repository에 임시 데이터를 추가할 수 있는 addTasks 메서드를 구현하고자 합니다.
물론 이미 구현되어 있는 saveTask 메서드를 여러 번 호출하면 되지만... 귀찮으니 한 번에 여러 데이터를 추가할 수 있도록 하고 싶습니다!
👇🏻 요렇게
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
7. ViewModel에서 FakeRepository 사용하기
이전 DefaultTasksRepository의 테스트 코드를 작성했을 때 처럼 생성자 의존성 주입으로 ViewModel에서 FakeRepository를 사용할 수 있게끔 하고 싶습니다.
하지만 ViewModel에서는 그저 ViewModel 클래스를 변경하는 것으로는 안됩니다.😭
왜냐하면 실제 viewModel를 사용하는 쪽에서 viewModel을 직접 생성하는 것이 아닌 아래와 같이 사용하기 때문입니다.
private val viewModel by viewModels<TasksViewModel>()
이러한 문제(?)를 해결하기 위해 사용할 수 있는 것이 바로 ViewModelProvider.Factory입니다.👍🏻
7.1 ViewModelFactory
ViewModelFactory를 만들기 전에 선행적으로 TasksViewModel을 생성자 의존성 주입 받을 수 있도록 바꿔줍니다.
// 이전
class TasksViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
}
// 변경
class TasksViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() {}
viewModel의 생성자를 변경했기 때문에, factory를 이용해 TasksViewModel를 생성해줄 필요가 있습니다.
TasksViewModel과 동일한 파일에 아래와 같이 Factory 클래스를 정의해주겠습니다.
(물론 다른 파일에 정의해도 문제 없습니다!)
@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory(
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return TasksViewModel(tasksRepository) as T
}
}
👆🏻 ViewModel이 어떻게 생성되는지 변경할 수 있는 가장 기본적인 방법입니다.🤗
이제 viewModel은 Factory를 통해 생성되도록 하였으므로, 실제 viewModel를 사용하는 쪽에 코드도 변경해주어야 합니다.
바로 TasksFragment입니다.
// 이전
private val viewModel by viewModels<TasksViewModel>()
// 변경 후
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
🌹 프로젝트 구조를 변경하였을 때는 반드시 앱을 직접 실행해 정상적으로 작동하는지 확인해봐야 합니다!
7.2 TasksViewModelTest에서 FakeTestRepository 사용하기
ViewModelProvider.Factory를 사용하여 의존성을 외부에서 주입할 수 있도록 수정하였습니다.
예전에 작성해둔 TasksViewModelTest 클래스 내에서 FakeTestRepository를 사용해보도록 하겠습니다.
private lateinit var tasksRepository: FakeTestRepository
private lateinit var tasksViewModel: TasksViewModel
@Before
fun setupViewModel() {
tasksRepository = FakeTestRepository()
val task1 = Task("Title1","Description1")
val task2 = Task("Title2","Description2")
val task3 = Task("Title3","Description3")
tasksRepository.addTasks(task1,task2,task3)
// 1. Given: a fresh TasksViewModel
tasksViewModel = TasksViewModel(tasksRepository)
}
🌹 테스트에서는 Delegate property와 ViewModelProvider를 사용하지 않고, 바로 생성하는 것에 유의하자.
더 이상 ViiewModel를 생성하기 위해 AndroidX Test 라이브러리를 사용하지 않으므로, 클래스 상위의 @RunWith(AndroidJUnit4::class) 어노테이션도 지워준다.
결과적으롤 이제 실제 production에서 사용하는 DefaultTasksRepository 대신 가짜로 만든 FakeTestRepository를 사용해 테스트를 할 수 있는 준비가 되었습니다.
이번에는 FakeRepository를 만드는 방법과
ViewModel를 생성자 의존성 주입을 사용할 수 있도록 수정하는 방법에 대해서 학습해 보았습니다.
다음은 이렇게 ViewModel를 통해 UI가 적절히 변경되는지 Fragment 단에서 테스트해보는
Integration test에 대해서 학습해볼 예정입니다.😌
감사합니다.😆
'Android > Android' 카테고리의 다른 글
[Android:Codelab] Dependency Injection and Test Doubles - 5 (0) | 2023.01.26 |
---|---|
[Android:Codelab] Dependency Injection and Test Doubles - 4 (0) | 2023.01.25 |
[Android:Codelab] Dependency Injection and Test Doubles - 2 (0) | 2023.01.24 |
[Android:Codelab] Dependency Injection and Test Doubles - 1 (0) | 2023.01.24 |
[Android:Codelab] Testing Basic-4 (0) | 2023.01.24 |