Android TDD CodeLab 02.Introduction to Test Doubles and Dependency Injection 1~5
클래스의 일부(메소드 또는 작은 메서드 모음)에 대한 단위 테스트를 작성할 때 목표는 해당 클래스의 코드만 테스트하는 것입니다.
DefaultTaskRepository.kt를 봅시다.
이 repository의 메소드를 단위 테스트 하고 싶지만, LocalTaskDataSource 및 RemoteTaskDataSource가 DefaultTasksRepository에 종속되어 있기 때문에 이 repository의 함수만 테스트 하는 것은 쉽지 않습니다.
DefaultTaskRepository의 메소드를 테스트 하기 위해, DataBase를 만들어야 합니까?
그러면 이 테스트는 로컬 테스트 일까요 계측 테스트 일까요?
TasksRemoteDataSource에 종속되어 있는 함수를 테스트 하기 위해 실제 네트워크와 통신을 해야할까요?
이런 문제를 해결하기 위해, TestDouble이라는 것을 알아봅시다.
테스트 더블은 테스트를 위해 특별히 제작된 클래스입니다. 테스트에서 클래스의 실제 버전을 대체하기 위한 것입니다.
Android에서 사용되는 가장 일반적인 테스트 더블은 Fakes와 Mocks입니다.
이 작업에서는 실제 DataSource에서 분리된 DefaultTasksRepository 단위 테스트를 위해 FakeDataSource testDouble을 만들 것입니다.
새 클래스 FakeDataSource를 testDouble로 사용하려면 TasksLocalDataSource와 TasksRemoteDataSource를 대체 할 수 있어야 한다.
두 DataSource 모두 TasksDataSource를 구현하고 있다. 따라서, FakeDataSource도 TasksDataSource를 구현하자. 그리고 모든 메소드를 override하자.
class FakeDataSource : TasksDataSource {
}
FakeDataSource는 Fake라고 하는 특정 유형의 testDouble입니다. Fake는 클래스의 "작동" 구현이 있는 테스트 더블이지만 테스트에는 적합하지만 프로덕션에는 적합하지 않은 방식으로 구현됩니다. "작업" 구현은 클래스가 입력이 주어지면 실제 출력을 생성한다는 것을 의미합니다.
예를 들어 Fake 데이터 원본은 네트워크에 연결하거나 데이터베이스에 아무 것도 저장하지 않고 메모리 내 목록만 사용합니다. 이것은 작업을 가져오거나 저장하는 메서드가 예상한 결과를 반환한다는 점에서 "예상한 대로 작동"하지만 서버나 데이터베이스에 저장되지 않기 때문에 프로덕션 환경에서는 이 구현을 사용할 수 없습니다.
//데이터베이스 또는 서버 응답을 "Fake" 처리하는 작업 목록입니다.
//현재 목표는 저장소의 getTasks 메소드를 테스트하는 것입니다.
//이것은 데이터 소스의 getTasks, deleteAllTasks 및 saveTask 메소드를 호출합니다.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
package com.example.android.architecture.blueprints.todoapp.data.source
import androidx.lifecycle.LiveData
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Task
//DefaultTasksRepository를 테스트하기 위해
//tasksRemoteDataSource와 tasksLocalDataSource를 대체하는 'fake' dataSource
//tasksRemoteDataSource와 tasksLocalDataSource 둘다 TasksDataSource를 implement함
//FakeDataSource도 TasksDataSource를 Implement 하자
//fake test double ->
// fake는 클래스의 "작동" 구현이 있는 테스트 더블
// 테스트에는 적합하지만 프로덕션에는 적합하지 않은 방식으로 구현됩니다.
// "작업" 구현은 클래스가 입력이 주어지면 실제 출력을 생성한다는 것을 의미합니다.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource {
override fun observeTasks(): LiveData<Result<List<Task>>> {
TODO("Not yet implemented")
}
override suspend fun getTasks(): Result<List<Task>> {
//작업이 null이 아닌 경우 성공 결과를 반환합니다. 작업이 null이면 오류 결과를 반환합니다.
tasks?.let { return Result.Success(ArrayList(it)) }
return Result.Error(
Exception("Tasks not found")
)
}
override suspend fun refreshTasks() {
TODO("Not yet implemented")
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
TODO("Not yet implemented")
}
override suspend fun getTask(taskId: String): Result<Task> {
TODO("Not yet implemented")
}
override suspend fun refreshTask(taskId: String) {
TODO("Not yet implemented")
}
override suspend fun saveTask(task: Task) {
tasks?.add(task)
}
override suspend fun completeTask(task: Task) {
TODO("Not yet implemented")
}
override suspend fun completeTask(taskId: String) {
TODO("Not yet implemented")
}
override suspend fun activateTask(task: Task) {
TODO("Not yet implemented")
}
override suspend fun activateTask(taskId: String) {
TODO("Not yet implemented")
}
override suspend fun clearCompletedTasks() {
TODO("Not yet implemented")
}
override suspend fun deleteAllTasks() {
tasks?.clear()
}
override suspend fun deleteTask(taskId: String) {
TODO("Not yet implemented")
}
}
현재 종속성은 DefaultTasksRepository의 init 메소드 내에서 구성됩니다.
DefaultTasksRepository 내에서 taskLocalDataSource 및 tasksRemoteDataSource를 만들고 할당하기 때문에 기본적으로 하드 코딩됩니다. tasksRemoteDataSource와 tasksLocalDataSource을 FakeDataSource로 바꿀 수 없습니다.
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
}
클래스 내에서 직접 dataSource를 생성하는 대신에, 생성자에서 수동 종속성 삽입을 통해 dataSource를 주입합니다.
// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }
// WITH
//1. 생성자에서 종속성 주입
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
//2. 더 이상 init 에서 datasource를 만들지 않으므로 init 메소드 삭제
//getRepository 메소드 수정
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
}
}
}
}
DefaultTasksRepository를 block 지정 후 command+N -> test 에 테스트 클래스를 만든다.
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() {
//fakeDataSource를 이용해서 테스트용 remote,local dataSource 만들기
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// fakeDataSource를 이용하여 테스트용 tasksRepository만들기
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() = runTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Result.Success
// Then tasks are loaded from the remote data source
assertEquals(tasks.data, remoteTasks)
}
getTasks()가 suspend fun 이기 때문에, 코루틴 범위가 필요하다.
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))
}
}