안드로이드 테스팅 입문하기

CmplxN·2020년 10월 9일
0

Testing

목록 보기
1/2
post-custom-banner

어떻게 어떻게 우여곡절 끝에 코딩을 마치면 내가 짠 코드가 예상대로 동작하는지 확인해야 한다. 대충 돌아가네~ 하고 배포하는 것은 앱 속에 폭탄을 심어 배포하는 것이나 마찬가지다. 그러므로 내 코드를 검증(테스팅)하는 과정을 거쳐야한다.

사실 한두번 테스팅 하는 것이야 손으로 직접 해도 괜찮을 수 있다. 그러나 꼼꼼하게 검증하기 위해서는 테스트 케이스가 많이 필요하다. 또한, 코드 변경이 잦을 수록 그만큼 같은 테스트를 여러번 해야한다. 이처럼 테스팅을 매번 사람 손으로 하는 것은 매우 비효율적이다.

테스팅을 하는 코드를 미리 짜둬 자동화하면, 테스팅을 편하게 할 수 있을 것이다.

구글에서 제공해주는 codelabs을 따라하며 테스팅에 입문해보자.

안드로이드 테스트

안드로이드 프로젝트를 생성하면 기본적으로 아래와 같은 프로젝트 구조로 생성될 것이다.
프로젝트 구조
위 사진처럼 java 아래에 총 3가지 항목이 생긴다.

  1. 실제 앱을 구성하는 코드
  2. androidTest : 안드로이드 디바이스(실제 기기, 에뮬레이터)를 이용한 테스트. 쉽게 생각하면 UI 테스트할 때 쓰인다.
  3. test : 디바이스가 필요없는 테스트. 기본적인 Unit Test부터 안드로이드 구성요소를 사용하는 테스트까지 가능하다.

이번 글에서는 기본적인 유닛테스트를 해볼것이다. (test)

ExampleUnitTest 뜯어보기

그러면 기본적으로 제공되는 ExampleUnitTest.kt를 살펴보자.

class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}

먼저 addition_isCorrect()라는 함수에 @Test 어노테이션이 붙어있다. 이는 안드로이드에서 기본적으로 사용하는 테스트 툴인 JUnit의 어노테이션으로, 함수가 테스트 함수라는 것을 알려준다.
그리고 addition_isCorrect()를 살펴보면 assertEquals(4, 2 + 2)가 있다. 짐작할 수 있겠지만 첫번째 인자는 기댓값(expected)이고, 둘째 인자는 실제 값(actual)이다. assertEquals()의 경우 기댓값과 실제 값이 같으면 테스트 통과. 다를시 테스트에 실패하는 구조다.

JUnit을 이용한 테스팅은 기본적으로 위와 같은 구조로 assertXXX함수를 이용해 기댓값과 실제값을 비교하여 테스팅 통과 여부를 정한다. 그외에도 @Test함수를 돌리기 전의 조건, 돌린 이후의 동작 등을 정의한 코드로 이루어진다.

테스트를 돌려서 성공하면 아래와 같이 테스트가 성공했다고 뜨고,
성공
테스트를 돌려서 실패하면 아래와 같이 실패한다고 확인할 수 있다. (actual을 2 + 3으로 변경)
실패

첫 테스트 작성하기

그러면 StatisticsUtils.kt에 있는 getActiveAndCompletedStats()를 이용해서 간단한 유닛 테스트를 작성해보자.

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

getActiveAndCompletedStats()를 간단히 설명하자면, List<Task>를 전달하면 완료한 Task의 비율과 현재 진행중인 Task의 비율을 리턴하는 함수다.
프로젝트 전체 코드는 링크에서 다운받아 확인하면 된다.

테스팅 클래스 만들기.

우선 테스팅을 위한 클래스를 만들어야 한다.
test 디렉터리 아래에 직접 만들어도 되지만, 안드로이드 스튜디오의 기능을 사용하면 편하게 만들 수 있다.

getActiveAndCompletedStats()에서 우클릭 -> Generate -> Test -> OK -> test 선택(androidTest가 있는 디렉토리가 아님)

그러면 원래 StatisticsUtils.kt와 같은 패키지 구조로 테스팅 클래스가 생성된다.

테스팅 작명 관련

사실 테스팅 클래스와 테스팅 함수의 이름은 아무렇게 해도 상관없다. 하지만, 컨벤션이 있으므로 웬만하면 지켜주는게 좋다.

클래스

일단 테스팅 클래스의 이름은 클래스의 이름 또는 파일의 이름(코틀린은 파일명이랑 클래스명이 다를 수 있다.) 뒤에 Test를 붙여 이름을 짓는다.
예를 들면 테스트할 클래스의 이름이 Example이면 테스팅 클래스의 이름은 ExampleTest가 되는 것이다.
참고로 예제의 getActiveAndCompletedStats()는 class 내부가 아니기 때문에 Generate를 통해 클래스를 만들면 StatisticsUtilsKtTest가 된다.

함수

테스트 함수는 다음과 같은 구조로 작명하면 된다. subjectUnderTest_actionOrInput_resultState

  1. subjectUnderTest : 테스트 대상이 되는 클래스나 함수명을 적는다.
  2. actionOrInput : 테스트에서의 action 또는 input를 적는다.
  3. Then : 작성한 테스팅 함수에서의 기댓값을 작성한다. (assert의 expected)

테스트 함수 작성하기

테스트 함수도 작성하는 기본적인 틀이 있는데 아래와 같은 구조를 가진다.

  1. Given : 실제 테스팅에 필요한 action 전의 객체를 만드는 등의 set-up을 한다.
  2. When : 실제 테스팅 하려는 action을 진행한다.
  3. Then : assert 부분으로 앞의 Given과 When으로 어떤 결과가 나올지 작성한다.

이제 실제로 getActiveAndCompletedStats()를 테스팅하는 코드를 보자.

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
        // Given
        val tasks = listOf(
            Task("title", "desc", isCompleted = true)
        )
        // When
        val result = getActiveAndCompletedStats(tasks)

        // Then
        assertEquals(result.activeTasksPercent, 0f)
        assertEquals(result.completedTasksPercent, 100f)
    }
}

앞서 말한 Given - When - Then의 구조로 위 코드를 살펴보자.

  1. tasks라는 List를 만들면서 action 전의 set-up을 한다.(Given)
  2. 그리고 난 뒤, 테스팅할 action인 getActiveAndCompletedStats() 호출을 시행한다.(When)
  3. 그리고 마지막으로 action의 결과를 assert로 검증한다.(Then)

그리고 위의 테스트가 통과됨을 확인할 수 있다.

테스팅을 통해 코드 개선하기

실패하는 테스트 케이스 발견

사실 위의 getActiveAndCompletedStats()는 잘못된 함수다. 아래 테스트 코드를 실행해보면 알 수 있다.

@Test
fun getActiveAndCompletedStats_empty_returnsZeros() {
    val tasks = listOf<Task>()
 
    val result = getActiveAndCompletedStats(tasks)
    
    assertThat(result.activeTasksPercent, `is`(0f))
    assertThat(result.completedTasksPercent, `is`(0f))
}

fail
getActiveAndCompletedStats()를 다시한번 살펴보면 테스트 에러메시지가 이야기하듯

val totalTasks = tasks!!.size // size 0
val activePercent = 100 * numberOfActiveTasks / totalTasks // divide by zero

부분에서 0으로 나누는 것에 대한 예외처리가 되어있지 않아 테스트에 실패한다.
이것 말고도 tasks가 null인 경우에도 NPE를 뿜으며 실패하게 될 것이다.

코드 개선 및 테스트 통과

테스팅을 통해 발견한 NPE와 DivZero를 개선한 코드를 작성하자.

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
    return if (tasks == null || tasks.isEmpty()) // null과 빈 List 예외처리
        StatsResult(0f, 0f)
    else {
        val totalTasks = tasks!!.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
                activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
                completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}

if절을 추가해서 null과 빈 리스트의 경우 완성률과 진행률을 0으로 해서 리턴하는 방식으로 코드를 수정했다.
이제 다시한번 테스팅을 돌려보자.

참고로 같은 테스트 클래스(StatisticsUtilsTest)에서 여러개의 @Test 함수를 만들고
테스트 클래스를 run(재생 아이콘)하면 테스트 클래스내의 모든 테스트 함수가 실행된다.

success
별도로 작성한 테스트코드를 포함해 5개의 테스트 케이스를 성공적으로 통과한 것을 확인할 수 있다.

맺음글

여기까지 JUnit을 이용한 기본적인 유닛테스트를 해보았다. 예시로 든 함수의 경우 간단하기 때문에 테스팅이 필요없을 수도 있다.
하지만 앞서 말한대로 프로젝트의 규모가 커지거나 변경이 잦으면 파급효과를 가늠하기 어렵다. 이럴 때 자동화된 검증과정을 거친다면 손쉽게 신뢰성을 제고할 수 있을 것이다.

사실 여기까지는 Android와 무관하게 JVM 언어를 사용한다면 공통적으로 할 수 있는 예제다. 다음 글에서는 본격적으로 안드로이드 구성요소(예를 들면 context 라던가)를 이용한 테스팅을 해볼 것이다. (아마 다다음 글에서 UI까지 포함한 테스트를 할 것 같다.)

profile
Android Developer
post-custom-banner

0개의 댓글