[Android Test] 안드로이드 Test Double & Dependency Injection Basic
Test Double
Android 앱의 한 Component 에 대한 Unit Test 를 작성하는 경우, 다른 Component 의 동작을 제외한 해당 Component 만의 Test 를 작성하는 것이 중요하다. 하나의 Component 가 동작하기 위해서 필요한 다른 모든 Component 를 해당 Component 의 Dependency(의존성) 이라고 한다.
Component 의 Unit Test 를 작성할때 다른 Dependency 에 대한 테스트를 제외해야 하는 이유는 다음과 같다.
- Test Speed : 만약 테스트할 Component 의 코드는 에뮬레이터 없이 실행될 수 있지만, 해당 Component 의 Dependency 들이 실행될 때 에뮬레이터가 필요한 경우, Component 의 Unit Test 를 위해 불필요하게 Instrumented Test 를 작성해야 하고, 결과적으로 느리게 실행되는 테스트를 작성하게 된다.
- Flaky Test : Component 의 Dependency 가 네트워크 호출을 한다던가, DB 연산을 수행하는 경우 Dependency 코드는 느리기만 할 뿐만 아니라 네트워크 환경에 따라서 종종 실패로 끝날 수 있다. 결과적으로 Component 의 Unit 테스트도 실행때마다 성공하거나 실패가 될 수 있다. 이렇듯 여러번의 테스트 실행에서 성공하거나 실패의 결과가 번갈아 나타날 수 있는 테스트를 Flaky 하다고 한다. 이런 결과는 테스트 결과의 신뢰성을 떨어뜨리게 된다.
- 실패를 일으키는 원인을 찾아내기 힘듬 : 만약 테스트가 실패한 경우, 해당 실패의 원인이 테스트 대상인지 아니면 Dependency 내부인지 쉽게 판단할 수 없다.
- Unit Test 를 작성할 Component 의 Dependency 를 테스트에서 제외하기 위한 방법으로 Test Double 을 사용한다. Test Double 은 테스트를 위해서 작성된 Component Dependency 를 의미한다. Test Double 에는 다음과 같은 종류가 있다.
- Fake : Working implementation of the class, but it's implemented in a way that makes it good for tests but unsuitable for production. Working implementation 이라는 말은 해당 Test Double 이 주어진 input 에 대해서 적절한 output 을 생성한다는 의미이다. 그러나 내부 구조는 실제 프로덕션에서는 사용할 수 없는, 오로지 테스트만을 위한 구조로 되어 있다.
- Mocking : A test double that tracks which of its methods were called. It then passes or fails a test depending on whether it's methods were called correctly. 어떤 메소드가 호출되었는지를 traking 할 수 있는 객체이다. 쉽게 Fake Implementation 을 만들 수 없는 클래스나 api 가 자주 바뀌지 않는 대상에 대해서 작성할 수 있다.
- Stub : A test double that includes no logic and only returns what you program it to return. A
StubTaskRepository
could be programmed to return certain combinations of tasks fromgetTasks
for example. 그냥 일정한 리턴만 주는 Test Double 인듯 하다. - Dummy : 메소드 인자의 전달만을 위해서 만들어지는 Test Double. 구체적인 구현 코드는 없다.
- Spy : 어떤 메소드가 몇번 불려졌는지 등을 Tracking 하는 Test Double.
관련 포스팅
Testing on the Toilet: Know Your Test Doubles
By Andrew Trenk This article was adapted from a Google Testing on the Toilet (TotT) episode. You can download a printer-friendly version ...
testing.googleblog.com
Dependency Injection Basic
Test Double 을 테스트하려는 Component 에서 사용하기 위해서는 이 Test Double 을 실제 Dependency 와 바꿔치기 하는 방법이 있어야 한다. 프로그래밍에서 Dependency 를 제공하는 방법은 다음과 같다.
- Hard Coding : Component 내부에서 Dependency 를 직접 생성하여 사용하는 것이다. 이같은 방법은 Dependency 를 Test Double 로 바꾸기 힘들다.
- 생성자의 인자로 전달하는 방법 : Component 의 Dependency 를 생성자의 인자 형태로 전달하는 것이다.
- Setter 메소드를 제공하는 방법 : Component 의 Dependency 를 제공할 수 있는 Setter 메소드를 구현하여 사용하는 방법이다.
위에서 2, 3번 방법을 외부에서 Dependency 를 주입한다는 뜻에서 Dependency Injection 이라고 한다. 또한 2번을 통한 Dependency Injection 을 Contructor Injection 이라 하고, 3번을 통한 방법은 Setter Injection 이라고 한다. 테스트 코드를 작성하기 위해서는 Component 가 Dependency Injection 을 활용하여 Dependency 를 전달받도록 하는 것이 좋다.
보통 Constructor Injection 을 Setter Injection 보다 우선하여 사용할 것을 고려해야한다.
- 생성자 호출 타이밍에 Injection 이 이루어지기 때문에 프로그래머가 Test double 을 전달하는 것을 잊어버리지 않을 수 있다.
- Setter Injection 을 활용하는 경우 Dependency 가 바뀔 수 있는 형태이기 때문에 몇몇 상황이 지난 후에서는 Component 가 적절하지 못한 State 로 변하여 테스트 하기 힘들어질 수 있다.
테스트 코드 상에서 Dependency 를 Test Double 로 바꿔 전달하기 위해서는 먼저 Dependency 의 형태를 interface 를 구현하는 형태로 바꾼뒤, 생성자의 파라미터 타입을 interface 형태로 변경하는 과정이 필요하다.
ViewModel Dependecy Injection
DI 위해 ViewModel 을 생성자에 인자가 존재하는 형태로 변경하기 위해서는 다음과 같은 과정이 필요하다.
- 실제 프로덕트 코드에서 ViewModel 의 생성자 호출은 보통 안드로이드 시스템에 의해서 이루어지기 때문에 이 시점에서 Dependency 를 전달할 방법이 있어야 한다.
- 이같은 방법이 ViewModelFactory 를 구현하는 것이다.
- 다음과 같은 형태로 구현할 수 있다.
@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory(
private val tasksRepository: TasksRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TaskDetailViewModel::class.java)) {
return TaskDetailViewModel(tasksRepository) as T
} else {
throw IllegalArgumentException()
}
}
}
이 후 Fragment 에서는 다음과 같은 형태로 사용할 수 있다.
class TaskDetailFragment : Fragment() {
// Delegation 을 사용하는 방법
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModel.TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
...
private lateinit var viewModel: TaskDetailViewModel
private fun initViewModel() {
val tasksRepository = DefaultTasksRepository.getRepository(requireActivity().application)
val viewModelFactory = TaskDetailViewModel.TaskDetailViewModelFactory(tasksRepository)
viewModel = ViewModelProvider(this, viewModelFactory).get(TaskDetailViewModel::class.java)
}
}
runBlockingTest
테스트 코드에서 코루틴 suspend 함수를 호출해야할때 사용할 수 있다. 호출하는 코루틴 함수가 synchronous 하고 deterministic 한 방법으로 실행될 수 있도록 도와준다. 실험적인 api 이기 때문에 @ExperimentalCoroutinesApi
를 사용해야한다. 또한 다음 dependency 를 추가해야 한다.
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"