Unit Test

don9wan·2021년 10월 5일
0

협업과 통합

목록 보기
12/12
post-thumbnail

필자는 Android Architecture Pattern, CI/CD 등 안드로이드 개발 현업에 대해 알아보고 있다. 보면 볼수록 unit test, ui test, testable code.. Test라는 단어가 눈에 계속 밟힌다. 그거 어떻게 하는건데. 오늘은 testable code를 작성하기 위한 초석을 다져보겠다. 테스트에 대해 알아보자.

안드로이드 문서 - 앱 테스트하기

Android 스튜디오는 테스트 작업을 간단하게 수행할 수 있도록 설계되었다.
몇 번의 클릭만으로 로컬 JVM에서 실행되는 JUnit 테스트나 기기에서 실행되는 계측 테스트를 설정할 수 있다.

테스트 코드의 위치는 작성한 테스트의 유형에 따라 결정된다.

  • 계측 테스트(androidTest) - UI test
    • 하드웨어 기기나 에뮬레이터에서 실행되는 테스트이다.
    • 안드로이드 프레임워크에 종속성이 있는 테스트이다.
    • 사용자 상호작용을 실행하는 Espresso, UI Automator 테스트 확장
  • 로컬 단위 테스트(test) - Unit test
    • 컴퓨터의 로컬 JVM(Java Virtual Machine)에서 실행된다.
    • 안드로이드 프레임워크와 관련없이 할 수 있는 테스트이다.
    • Android API 호출을 테스트하는 Mockito 테스트 확장

android에 보면 test를 작성하는 위의 2가지 디렉토리(계측, 로컬)가 있는데, 여기에 test 파일을 작성해서 테스트를 진행한다. 가장 대표적인 테스트 프레임워크로 Mockito가 있다. 이 Mockito는 Mocking이라는 작업과 연관이 있다. Mocking은 실제 값으로 테스트를 하기 어려울 때 가짜 값을 사용하여 테스트를 진행할 수 있게 해주는 방법이다.

Unit Test vs UI Test

Unit Test

  • 일반적으로 코드의 유닛 단위(메소드, 클래스, 컴포넌트)의 기능을 실행하는 방식
  • 관련 툴 : JUnit, Mockito, PowerMock

UI Test

  • 사용자 인터랙션(버튼 클릭, 텍스트 입력 등)을 평가
  • 관련 툴 : Espresso, UIAutomator, Robotium, Appium, Calabash, Robolectric

일반 클래스 + Truth

  • Unit Tested class : MyCalc
    • create Test class : MyCalc 우클릭 -> Create test -> Testing library(Truth) 설정 -> MyCalcTest -> create
  • Unit Testing class : MyCalcTest
    • @Test 어노테이션이 붙여진 메서드들 작성하는 클래스.
    • @Before, @After -> 테스트 함수 실행 전/후 실행할 로직
    • @Test -> @Before 이후 테스트 실행할 로직
  • Test process
    • 먼저 MyCalc() 클래스를 테스트할거니깐 @Before에 미리 클래스를 생성
    • (1) 해당 클래스의 함수에 원하는 함수를 호출 후**
    • (2) Truth 테스팅 라이브러리의 assertThat(), isEqualTo()와 같은 함수를 활용하여 내가 원하는 값이 나오는지 테스트
    • 테스트 통과한 것은 초록색 실패는 빨간색

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 + LiveData + Mockito + Junit4 + Truth

보통 ViewModel은 로직과 값을 갖고 있는 클래스이기 때문에 안드로이드 테스트를 활발하게 사용한다. ViewModel의 함수가 호출된 후 LiveData에 값이 잘 세팅 되는지 아니면 어떤 에러가 나는지 테스팅한다.

  • 기존엔 MyCalc 클래스의 계산결과를 바로 테스팅했다면
  • 이번엔 ViewModel 클래스에서 MyCalc 클래스의 계산결과를 LiveData에 세팅해주는 로직이 추가되었고
  • 이 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")
    }
}

Why?

  • 생산성 증대 : 확신을 가지고 시스템 개발 가능
  • 수월한 디버깅
  • 수월한 코드 변경(특히 refactoring)
  • modular한 설계
  • 협업
    • code owner가 아니더라도 코드 수정 가능
    • 테스트 코드, 테스트 케이스만 봐도 시스템의 기능과 의도 및 올바른 사용법 파악 가능
profile
한 눈에 보기 : https://velog.io/@dongwan999/LIST

0개의 댓글