필자는 Android Architecture Pattern, CI/CD 등 안드로이드 개발 현업에 대해 알아보고 있다. 보면 볼수록 unit test, ui test, testable code.. Test라는 단어가 눈에 계속 밟힌다. 그거 어떻게 하는건데. 오늘은 testable code를 작성하기 위한 초석을 다져보겠다. 테스트에 대해 알아보자.
Android 스튜디오는 테스트 작업을 간단하게 수행할 수 있도록 설계되었다.
몇 번의 클릭만으로 로컬 JVM에서 실행되는 JUnit 테스트나 기기에서 실행되는 계측 테스트를 설정할 수 있다.
테스트 코드의 위치는 작성한 테스트의 유형에 따라 결정된다.
android에 보면 test를 작성하는 위의 2가지 디렉토리(계측, 로컬)가 있는데, 여기에 test 파일을 작성해서 테스트를 진행한다. 가장 대표적인 테스트 프레임워크로 Mockito가 있다. 이 Mockito는 Mocking이라는 작업과 연관이 있다. Mocking은 실제 값으로 테스트를 하기 어려울 때 가짜 값을 사용하여 테스트를 진행할 수 있게 해주는 방법이다.
Unit Test
UI Test
MyCalc.kt
class MyCalc : Calculations { private val pi = 3.14 override fun calculateCircumference(radius: Double): Double { return 2 * pi * radius } override fun calculateArea(radius: Double): Double { return pi * radius * radius } }
Truth
Guava 팀에서 개발한 Assertion 테스팅 라이브러리
JUnit4의 assert문을 대체 사용할 수 있으며 다양한 기능과 가독성 좋은 테스팅 코드 작성 가능MyCalcTest.kt
@Before fun setUp() { myCalc = MyCalc() } @Test fun calculateCircumference_radiusGiven_returnsCorrectResult(){ val result = myCalc.calculateCircumference(2.1) Truth.assertThat(result).isEqualTo(13.188) } @Test fun calculateCircumference_zeroRadius_returnsCorrectResult(){ val result = myCalc.calculateCircumference(0.0) Truth.assertThat(result).isEqualTo(0.0) } . .
보통 ViewModel은 로직과 값을 갖고 있는 클래스이기 때문에 안드로이드 테스트를 활발하게 사용한다. ViewModel의 함수가 호출된 후 LiveData에 값이 잘 세팅 되는지 아니면 어떤 에러가 나는지 테스팅한다.
CalcViewModel.kt
import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import java.lang.Exception //ViewModel 객체(클래스) class CalcViewModel( private val calculations: Calculations ) : ViewModel() { //LiveData는 abstract class이기 때문에 //LiveData Class를 상속받은 MutableLiveData를 사용한다. //MutableLiveData : 값 get/set 가능 ("Mutable") //LiveData : 값 get만 가능 //radius, area, circumference 변수 초기화 var radius = MutableLiveData<String>() var area = MutableLiveData<String>() var circumference = MutableLiveData<String>() //area, circumfrence는 라이브데이터에 바인딩 추가 코드 val areaValue: LiveData<String> get() = area val circumferenceValue: LiveData<String> get() = circumference fun calculate() { try { val radiusDoubleValue = radius.value?.toDouble() //radius가 null이 아니면 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 } } //기존 MyCalc 클래스의 메서드 1 fun calculateArea(radius: Double) { val calculatedArea = calculations.calculateArea(radius) area.value = calculatedArea.toString() } //기존 MyCalc 클래스의 메서드 2 fun calculateCircumference(radius: Double) { val calculatedCircumference = calculations.calculateCircumference(radius) circumference.value = calculatedCircumference.toString() } }
CalcViewModelTest.kt
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.Mockito class CalcViewModelTest{ private lateinit var calcViewModel: CalcViewModel private lateinit var calculations: Calculations // 규칙 추가 : 한개의 스레드에서만 실행하여 Syncronazation. @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp() { //Mocking할 클래스를 mock()해준다. (테스트를 위한 모의 객체) calculations = Mockito.mock(Calculations::class.java) //when() 안에서 테스트할 함수를 호출하고 //thenReturn()에는 내가 반환 받을 값을 명시한다. Mockito.`when`(calculations.calculateArea(2.1)).thenReturn(13.8474) Mockito.`when`(calculations.calculateCircumference(1.0)).thenReturn(6.28) //설정 후 (Mock) 객체 생성 calcViewModel = CalcViewModel(calculations) } //테스트 진행 @Test fun calculateArea_radiusSent_updateLiveData(){ calcViewModel.calculateArea(2.1) val result = calcViewModel.areaValue.value Truth.assertThat(result).isEqualTo("13.8474") } @Test fun calculateCircumference_radiusSent_updateLiveData(){ calcViewModel.calculateCircumference(1.0) val result = calcViewModel.circumferenceValue.value Truth.assertThat(result).isEqualTo("6.28") } }