[Android Test] @Before와 @Test를 이용한 클래스 테스트 진행
- 안드로이드 테스트 코드 작성에 대한 각종 내용을 기록하는 말머리 [Android Test] - 아래와 같이 원주율을 활용한 계산 Class가 있다.class MyCalc : Calculations { private val pi = 3.14 override fun calculateCircumfere
yongdragon9819.tistory.com
- 이전에 테스트 코드에 대한 기본 글을 작성했다.
- 오늘은 Test Double이란 개념을 알아보고, mock을 통해 ViewModel 테스트 코드를 작성해 봤다.
- 먼저 Test Double은 테스트를 위한 가짜 객체로, 실제 객체를 대체하여 예측 가능한 동작을 하도록 만들어진 것이다. 매번 전체 구현해서 테스트할 수 없으니 대역을 만들어 놓는 것이다.
- 실제 API나 DB 호출이 없이 테스트가 가능하도록 지원해 준다.
- 대표적으로 아래 5가지 종류가 있는데, Mock을 사용해 볼 것이다.
Dummy | 사용은 안 되지만 인자를 채우기 위해 필요 | null 대신 넘기는 빈 객체 |
Fake | 실제 동작을 흉내내는 간단한 구현 | 인메모리 DB |
Stub | 특정 입력에 대해 고정된 값을 리턴 | 항상 "Success"를 반환하는 함수 |
Spy | 실제 객체처럼 동작하지만 호출 기록을 추적함 | 함수가 몇 번 불렸는지 확인 |
Mock | 사전에 정해진 기대 행동대로 호출됐는지 검증 | 특정 함수가 1번 호출되었는지 검증 |
- 아래는 ViewModel로 구현될 인터페이스와 ViewModel 클래스이다.
interface Calculations {
fun calculateCircumference(radius:Double) : Double
fun calculateArea(radius:Double) : Double
}
class CalcViewModel(
private val calculations: Calculations
) : ViewModel() {
var radius = MutableLiveData<String>()
var area = MutableLiveData<String?>()
val areaValue: LiveData<String?>
get() = area
var circumference = MutableLiveData<String?>()
val circumferenceValue: LiveData<String?>
get() = circumference
fun calculate() {
try {
val radiusDoubleValue = radius.value?.toDouble()
if (radiusDoubleValue != null) {
calculateArea(radiusDoubleValue)
calculateCircumference(radiusDoubleValue)
} else {
area.value = null
circumference.value = null
}
} catch (e: Exception) {
Log.i("MYTAG", e.message.toString())
area.value = null
circumference.value = null
}
}
fun calculateArea(radius: Double) {
val calculatedArea = calculations.calculateArea(radius)
area.value = calculatedArea.toString()
}
fun calculateCircumference(radius: Double) {
val calculatedCircumference = calculations.calculateCircumference(radius)
circumference.value = calculatedCircumference.toString()
}
}
- ViewModel 테스트 클래스를 생성해 주고, 아래 어노테이션을 설정해 준다.
@RunWith(RobolectricTestRunner::class)
class CalcViewModelTest { ....
- JVM 환경에서 Android API를 테스트할 수 있도록 도와주는 Runner로서 LiveData, ViewModel, Looper 등 Android 컴포넌트를 사용할 수 있게 해 준다. 아래 내용을 gradle에 등록해주어야 한다.
testImplementation("org.robolectric:robolectric:4.10.3")
- 위에 올라간 ViewModel 코드를 살펴보면 LiveData를 사용 중인데, 실제 인앱 로직이 아니고 테스트에서는 즉시 확인을 위해 동기적으로 작동 처리를 한다. 이를 위해 InstantTaskExecutorRule()을 구성해주어야 한다.
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
- observe 즉시 호출 및 확인이 가능하다.
- 이제 이어서 대상 ViewModel과 인터페이스를 선언해 주고, 그 초기화를 위한 Before 작업 처리를 진행한다.
@RunWith(RobolectricTestRunner::class)
class CalcViewModelTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var calViewModel: CalcViewModel
private lateinit var calculations: Calculations
@Before
fun setUp() {
calculations = mock(Calculations::class.java)
Mockito.`when`(calculations.calculateArea(2.1)).thenReturn(13.8474)
Mockito.`when`(calculations.calculateCircumference(2.1)).thenReturn(13.1947)
calViewModel = CalcViewModel(calculations)
}
....
- 여기서 이제 mock()을 통해 인터페이스를 가상 객체로 구성한 것을 볼 수 있다.
- 그리고 테스트에서 예측 가능한 Behavior를 바로 구성해 준다. when() 내부 함수가 저렇게 호출되면 thenReturn()으로 이렇게 반환되는 것이 맞도록 설정한다.
- Mockito와 InstantTaskExecutorRule() 사용을 위해 아래 내용도 Gradle에 등록해주어야 한다.
testImplementation ("android.arch.core:core-testing:1.1.1")
testImplementation("org.mockito:mockito-core:1.10.19")
implementation ("org.mockito:mockito-core:5.3.1")
- 이제 assertEquals()를 통해 실제 검증 테스트가 가능하다.
@Test
fun `calArea radius given updates LiveData correctly`() {
calViewModel.calculateArea(2.1)
val result = calViewModel.areaValue.getOrAwaitValue()
assertEquals("13.8474", result)
}
@Test
fun `calCircumference radius given updates LiveData correctly`() {
calViewModel.calculateCircumference(2.1)
val result = calViewModel.circumferenceValue.getOrAwaitValue()
assertEquals("13.1947", result)
}
+ LiveData를 동기적으로 받아오기 위해 아래 확장함수를 구성했다..
fun <T> LiveData<T>.getOrAwaitValue(): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(value: T) {
data = value
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
return data ?: throw NullPointerException("LiveData value was null")
}
- 자잘한 경고가 뜨긴 하는데, 테스트 자체는 성공한 결과를 확인할 수 있다. 경고 내용들은 버전 업데이트에 대한 주의 정도인 듯..