개발에서 TDD라는 유명한 용어가 있다.
Test Driven Development의 약자로서 테스트를 먼저 만들고 테스트를 통과하기 위한 것을 짜는 것이다.
TDD의 장점은 결함(버그)가 줄어들고, 코드 복잡도가 떨어진다는 것이다.
거의 완성해가는 프로젝트에서 Unit Test를 하는 것이 TDD라고는 할 수는 없다. 하지만 앞으로의 프로젝트를 진행하는데 TDD를 하는 방법을 익힐 수 있을 것이다. BlindCommunity2 앱(모두의 취준생커뮤니티2)에서 Unit Test를 진행하여 보았다.
안드로이드에서 테스트는 크게 아래와 같이 3가지의 단계가 있다.
1. UI Test
실제 디바이스나 에뮬레이터로 UI를 테스트하는 것이다.
2. Integration Test
개발자가 작성한 UI를 제외한 코드가 Android Framework에서 어떻게 상호작용하는지 테스트하는 것이다.
3. Unit Test
코드의 유닛 단위(클래스, 메소드, 컴포넌트)의 기능을 테스트하는 것이다.
이 중에서 3번째인 Unit Test를 이번 포스팅에서 정리해보려 한다.
우선 용어부터 정리해보자
- 관심 객체 : 테스트 시점에 검증할 객체
- 외부 객체 : 관심 객체의 의존성에 해당하는 객체
- 모의 객체(Mock) : 실제 객체를 모방한 객체 (모방한 이유? 실제 객체를 만들기엔 비용과 시간이 많이 들거나 의존성이 길게 걸쳐져 있기 때문!)
- Stub : 모의 객체(Mock)의 실체를 임의로 정하는 행위
유닛 테스트를 처음 접할 때 어려웠던 것이 모의객체(Mock)와 Stub의 개념이었는데, 용도를 알면 이해가 쉽다.
내가 이해한 바로는 모의객체(mock)는 외부객체가 복잡한 경우 이의 인터페이스(껍데기)를 만들어준 것이다.
mock을 왜 만드느냐?하면 관심객체를 테스트할 때 관심객체를 테스트할 때 필요한 인자이기 때문이다.
그럼 껍데기 밖에 없는데 테스트가 제대로 되겠는가?하면 이때 등장하는게 Stub이다. 모의객체를 만들때 반환 값까지 같이 적어주는 함수가 Stub이다.
따라서 외부 의존성을 떼어놓고 딱 내가 관심있는 객체에 대해서 테스트할 수 있는 것이다.(얼마나 논리적인 행위인가..!)
테스트 코드에서는 다음 두 가지 내용의 검증이 필요하다.
- 관심 객체의 상태 변화
- 외부 객체의 함수 호출 여부
예를 들어 우리가 한 viewModel을 검증하고 싶으면 관심 객체는 ViewModel이고, 외부 객체는 Repository가 될 수 있다.
또는 retrofit2을 통해 api를 호출하는 repository가 있다면 실제 객체는 해당 repository고, 외부 객체는 api 인터페이스인 service가 될 수 있다.
먼저 추가한 dependency는 다음과 같다.
//Mockito
androidTestImplementation("org.mockito:mockito-android:2.24.5")
testImplementation 'org.mockito:mockito-inline:2.21.0'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation 'org.mockito:mockito-core:3.5.9'
Android Studio에서 프로젝트 창을 보면 androidTest가 있고, 그냥 test가 있다.
- androidTest의 파일들은 Android 디바이스 내에서 Ui를 테스트한다.
- test의 파일들은 UnitTest를 진행한다. 여기에 usecase폴더와 AbstractKoinTest파일을 만들자.
나는 Koin을 이용하여 의존성 주입을 추가하였기 때문에, KoinTest를 상속하는 AbstractKoinTest라는 추상클래스를 만들어 각각의 Unit Test 하려는 클래스가 이를 상속하게 하였다. Koin module을 로딩하는 클래스이다.
inline fun <reified T> mock(): T = Mockito.mock(T::class.java) 코드는 type과 상관없이 mock() 함수로 mock객체를 만들 수 있도록 해준다.
InstantTaskExecutorRule 이란, 모든 Architecture Components-related background 작업을 백그라운드에서가 아닌 동일한 Thread에서 돌게하여 동기적인 처리가 가능하도록 해주는 Rule이다. 이것이 없으면 여기 코드에서 MutableLiveData에 setValue() 해주는 부분에서 null pointer error가 발생한다.
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.yeonkyu.blindcommunity2.di.networkModule
import com.yeonkyu.blindcommunity2.di.repositoryModule
import com.yeonkyu.blindcommunity2.di.viewModelModule
import org.junit.Rule
import org.koin.core.logger.Level
import org.koin.test.KoinTest
import org.koin.test.KoinTestRule
import org.koin.test.mock.MockProviderRule
import org.mockito.Mockito
abstract class AbstractKoinTest: KoinTest {
inline fun <reified T> mock(): T = Mockito.mock(T::class.java)
@get:Rule
val koinTestRule = KoinTestRule.create {
printLogger(Level.DEBUG)
modules(listOf(repositoryModule, viewModelModule, networkModule))
}
@get:Rule
val mockProvider = MockProviderRule.create { clazz -> Mockito.mock(clazz.java)
}
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
}
유닛 테스트 맛보기 시작~
1. Repository Test
위에서 만든 AbstractKoinTest를 상속받은 PostRepositoryTest를 만들었다. 어노테이션 @Before 에소는 테스트를 진행하기 전에 초기화할 것 들을 작성하면 되고, @Test에서 본격적으로 테스트할 함수를 작성하면 된다.
아래는 postRepository의 getFreePost()에서 정의한 api를 호출하는 메소드를 테스트하였다.
assertTrue()의 argument값이 true이면 PASS, false면 FAIL의 테스트 결과가 나온다.
아래의 코드는 유닛테스트의 예시를 assertTrue 안에 ArrayList인지를 검증한다. (하지만 ArrayList인지 검증하는것보다는 그 내용물이 의도한것과 일치한지를 검증하는 것이 맞는 테스트인것 같다.)
import com.yeonkyu.blindcommunity2.AbstractKoinTest
import org.junit.Test
import com.yeonkyu.blindcommunity2.data.repository.PostRepository
import kotlinx.coroutines.*
import org.junit.Assert.assertTrue
import org.junit.Before
import org.koin.test.inject
class PostRepositoryTest: AbstractKoinTest() {
private val repository by inject<PostRepository>()
private val postId:String = "20201125_001913"
@Before
fun setup(){
//변수 초기화가 필요하다면 @Test 전에 세팅할 수 있다.
}
@Test
fun getFreePostTest() = runBlocking {
val response = repository.getFreePost(postId)
assertTrue(response is ArrayList<*>)
}
}
테스트를 할 준비를 마쳤으면 프로젝트 창에서 해당 클래스 위에서 오른쪽 마우스 클릭한 뒤 Run (또는 메소드 옆에 초록색 삼각형 클릭) 하면 유닛테스트가 진행된다.
유닛 테스트가 끝나면 결과가 Android Studio 의 Run창에 다음과 같이 뜨게 된다.
이런식으로 하나의 메소드를 테스트 해보았다. PASS가 떴다.
2. ViewModel Test
viewModel에서 외부 객체는 repository이다. Mockito 라이브라리를 이용하여 mock객체로 만들자.
@Before 에서는 viewModel의 데이터 초기화를 해주었다.
@Test 에서는 `when`은 해당 함수를 호출하면 결과를 무엇으로 줄 지 정하게 하는 코드이다.
따라서 다음 줄에서 refreshPost()를 call 하였고 함수 내부에서 repository의 getFreePost를 호출하였다.
마지막으로 verify()는 내가 원하는 결과값으로 흘러갔는지 검증하는 함수이다. getFreePost()가 1번만 호출되었는지 검증하는 verify문을 넣었다.
import com.yeonkyu.blindcommunity2.AbstractKoinTest
import com.yeonkyu.blindcommunity2.data.repository.PostRepository
import com.yeonkyu.blindcommunity2.ui.post.PostViewModel
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.*
class PostViewModelTest: AbstractKoinTest() {
private lateinit var postViewModel:PostViewModel
private val repository: PostRepository = mock()
@Before
fun setup(){
postViewModel = PostViewModel(repository)
postViewModel.type = 1
postViewModel.postId.value = "20201125_001913"
}
@Test
fun refreshPostTest(){
runBlocking {
`when`(repository.getFreePost("20201125_001913")).thenReturn(Unit)
postViewModel.refreshPost()
verify(repository, times(1)).getFreePost("20201125_001913")
}
}
}
아래 사진은 usecase폴더 전체를 UnitTest하여 PostViewModel과 PostRepository를 같이 테스트한 것이다.
PASS가 뜨면 아래와 같은 모습이다.
지금은 일부 메소드만 테스트한 것이지만 이러한 방식으로 모든 테스트할 유닛들을 정의하고 검증해야할 것 같다. (정말 많은 시간이 걸릴 것 같다)
후기
안드로이드 스튜디오에서 Koin과 함께 Unit Test를 정리한 사이트 찾기가 쉽지 않았는데 아직까지 테스트가 쉽지 않다고 느껴진다. 위의 예시처럼 하기 보다는 데이터가 의도한 데이터와 일치하는지도 테스팅하는게 '진짜 검증'이 아닐까 싶다. ViewModel에서 LiveData의 변화를 감지해서 이를 의도한 데이터와 맞는지 테스팅하는것도 하고 싶었지만 난이도가 조금 더 있는 것같다. 앞으로 더 많이 학습해야 할 분야인 것 같다.
참고한 사이트
https://insert-koin.io/docs/quickstart/junit-test
https://github.com/skydoves/MarvelHeroes
https://simplifyprocess.tistory.com/8?category=893223
'Programming > Android' 카테고리의 다른 글
Android Jetpack Paging 3 적용하기 (0) | 2021.09.03 |
---|---|
코루틴은 race condition이 발생하지 않는 것일까? (0) | 2021.06.16 |
[Android] ViewPager2 + BottomNavigation 으로 Fragment 관리하기 (0) | 2021.05.16 |
[Android] Recyclerview에 ListAdapter사용하기 (0) | 2021.04.29 |
MVVM에서 ViewModel의 Event 전달하기2(Event Wrapper) (0) | 2021.04.28 |