[codelab 5.1 - 5 6 7] Android Test 종류, [일반 클래스] Local Unit Test, TDD
Android/Android Test

[codelab 5.1 - 5 6 7] Android Test 종류, [일반 클래스] Local Unit Test, TDD

728x90
반응형

CodeLab 05.1 Testing Basics

 

목차

  1. (Task 5)Source sets : 계측(Instrumented) Test / 로컬(Local) Test
  2. (Task 6) [일반 클래스]를 로컬 단위 테스트(Local Unit Test) 하기
  3. (Task 7) 테스트 주도 개발(TDD) 체험하기

1) 안드로이드에서 테스트의 기초를 학습함을 목표로 합니다. 

  • Android에서 단위 테스트를 작성하고 실행하는 방법
  • 테스트 주도 개발(TDD)을 사용하는 방법
  • 계측 테스트(instrumented tests) 및 로컬 테스트(local tests)를 선택하는 방법

2) 학습하게 될 라이브러리

  • JUnit4
  • 햄크레스트(Hamcrest)
  • AndroidX 테스트 라이브러리
  • AndroidX Architecture 구성요소의 핵심 테스트 라이브러리 (android.arch.core.executor.testing)

3) 세부 목표

  • Android에서 계측 테스트(instrumented tests) 및 로컬 테스트(local tests)를 설정, 실행 및 해석
  • JUnit4 및 Hamcrest를 사용하여 Android에서 단위 테스트를 작성

Task 5 : Familiarizing yourself with the code

 

Source sets(소스 세트) - Instrumented Test / Local Test

  • Android Studio에서 Project를 생성하면 생기는 3개 폴터 세트를 말합니다. 
  • 소스 세트는 앱의 소스 코드와  테스트와 관련된 코드가 포함됩니다. 
  • 새 Android 프로젝트를 만들면 기본적으로 다음 세 가지 소스 세트가 제공됩니다. 

main: 앱 코드가 포함되어 있습니다. 이 코드는 빌드할 수 있는 모든 다른 버전의 앱 간에 공유됩니다 
androidTest계측 테스트와 관련된 테스트를 포함합니다.
test: 로컬 테스트와 관련된 테스트를 포함합니다.

 

로컬 테스트와 계측 테스트의 차이점 은 실행 방식에 있습니다.

 

 

Local tests(로컬 테스트)

  • 이러한 테스트는 개발 머신의 JVM에서 로컬로 실행되며 에뮬레이터 나 물리적 장치가 필요하지 않습니다.
  • 이로 인해 빠르게 실행되지만 실제 환경과 동일하게 작동되지 않습니다.
  • Android Studio에서 로컬 테스트는 녹색 및 빨간색 삼각형 아이콘으로 표시됩니다.

로컬 테스트 실행 성공
로컬 테스트 실행 실패

 

 

 

instrumented test(계측 테스트) 

  • 이 테스트는 실제 또는 에뮬레이트 된 Android 기기에서 실행되므로 실제 상황을 반영하지만 훨씬 느립니다.
  • 계측 테스트는 에뮬레이트 된 장치 나 실제 장치에서 실행되어야 하는 경우 사용합니다. 
  • 계측 테스트는 거의 항상 Android OS 또는 Android 프레임 워크를 사용합니다.
  • 이 테스트에서는 Context를 사용하여 Instrumentation 를 사용하고,  이를 통해 패키지 이름을 얻고 비교 할 수 있습니다.
  • Android Studio에서 계측 테스트는 녹색 및 빨간색 삼각형 아이콘이 있는 Android로 표시됩니다.

계측 테스트 실행 성공
계측 테스트 실행 성공


Task 5 : Writing your first test

일반 클래스 - 로컬 단위 테스트(Local Unit Test)

getActiveAndCompletedStats() 함수에 대하여 테스트를 수행하려 합니다. 

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
    val totalTasks = tasks!!.size
    val numberOfActiveTasks = tasks.count { it.isActive }
    return StatsResult(
        activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
        completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
    )
}

data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)

이 함수는 작업 목록 List<Task>을 tasks파라미터로 입력받고,  StatsResult 데이터 클래스를 리턴한다. 

StatsResult 데이터 클래스는 Tasks의 진행과 완료에 대한 확률 값을 나타내는 플롯 객체를 포함하고 있다. 

 

 

1 단계 : 테스트 클래스 만들기

 

함수 이름을 선택 후 오른쪽 버튼을 누르고 Generate > Tast를 클릭하고,

 

 

ClassName을 수정하고 ok버튼을 클릭합니다. 

그리고 Test(로컬 테스트)와 androidTest(계측 테스트) 중에서 선택하고 ok 버튼을 클릭합니다.

저의 경우 test를 선택했기 때문에 테스트 폴더에 해당 클래스가 생성된 것을 확인할 수 있었습니다. 

 

2 단계 : 테스트 함수 작성

정상적으로 작동되는 시나리오를 생각하고 그 시나리오를 코드로 작성해야 합니다. 

  1.  stap1> 시나리오는 입력값 설정
  2.  stap2> 테스트 대상 함수 호출
  3.  step3> 결과 확인

이 순서로 테스트 함수를 작성합니다. 

 

시나리오 1

completedTasks이 없고, activeTasks이 1개 있는 경우, 

-> activeTasksPercent값은 100%이고,  completedTasksPercent은 0%이여야 합니다.

 

시나리오 1을 코드로 작성하면 다음과 같이 테스트 함수를 구현할 수 있습니다.

class StatisticsUtilsTest{

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero(){

        // step1 : 테스트 대상 객체인 active task 생성
        val tasks = listOf<Task>(
                Task("title", "desc", isCompleted = false)
        )

        // step2 : 테스트 대상 함수 호출
        val result = getActiveAndCompletedStats(tasks)

        // step3 : 결과 확인
//        assertEquals(result.completedTasksPercent, 0f)
//        assertEquals(result.activeTasksPercent, 100f)
        assertThat(result.activeTasksPercent, `is`(100f)) // Hamcrest  사용
        assertThat(result.completedTasksPercent, `is`(0f))
    }
}

 

3 단계 : Hamcrest를 사용하여 어설 션(assert ) 작성

 

Hamcrest를 사용하면 가독성이 좋습니다. 

Hamcrest를 사용하기 위해 

app / build.gradle 에 종속성을 추가합니다. 

dependencies {
    //...
    testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}

 

 

 

정리 : 읽기 쉬운 테스트 작성 전략

더보기

읽기 쉬운 테스트를 작성하는 데 사용할 수 있는 몇 가지 다른 전략이 있습니다.

전략 1 : 테스트의 구조를 Given, When, Then 테스트 mnemonic(니모닉)을 따름  
이렇게 하면 테스트가 세 부분으로 나뉩니다.

step1 Given(주어진 사항) : 테스트에 필요한 개체와 앱의 상태를 설정합니다. 이 테스트에서 "주어진"것은 tasks이 active(isCompleted =true)된 작업 목록이 있다는 것입니다.
step 2 When(언제) : 테스트 중인 개체에 대해 실제 작업을 수행합니다. 이 테스트에서는 getActiveAndCompletedStats.
step3 Then(그런 다음) : 테스트가 통과 또는 실패했는지 확인하는 작업을 수행할 때 어떤 일이 발생하는지 실제로 확인하는 곳입니다. 이것은 일반적으로 여러 assert 함수 호출입니다. 이 테스트에서 올바른 활성 및 완료된 백분율을 확인하는 것은 두 개의 어설 션입니다.

cf) "Arrange, Act, Assert"(AAA) 테스트 니모닉은 유사한 개념입니다.

전략 2 : 테스트(함수) 이름

테스트 이름에 테스트하고자 하는 목적을 설명합니다.  
함수 명명 규칙을 예를 들어 설명하면 다음과 같습니다.

 

getActiveAndCompletedStats_noCompleted_returnsHundredZero()
subjectUnderTest_actionOrInput_resultState


getActiveAndCompletedStats -> 테스트 중인 function 또는 class 이름
noCompleted -> action 또는 input
returnsHundredZero) -> 예상 result


Task 7 : Writing more tests

테스트 주도 개발(TDD) 체험하기

TDD란 '기능 코드'를 작성하기 전에 먼저 '테스트 코드'를 먼저 작성하고,

이런 테스트 코드를 통과를 목표로 기능 코드를 작성하면서 개발하는 것을 말합니다. 

만약 목표로 하는 테스트코드 중 테스트 실패가되는 테스트가 있다면, 

그 테스트를 통과하기위한 최소한의 코드로 다시 작성(수정)합니다. 

이 작업을 모든 테스트가 통과할때까지 반복합니다. 

 

정리하면 TDD( Test Driven Development) 전략 단계는
1) Given, When, Then 구조를 사용하고 규칙을 따르는 이름으로 테스트를 작성합니다.
2) 테스트가 실패했는지 확인하십시오.
3) 테스트를 통과하려면 최소한의 코드를 작성하십시오.
4) 모든 테스트에 대해 반복하십시오

 

 

 

예시를 들어 테스트주도개발(TDD) 를 해보도록 하겠습니다. 

 

 

1 단계. 테스트 작성

 

Tast 1 : getActiveAndCompletedStats_noCompleted_returnsHundredZero()

completedTasks이 없고, activeTasks이 1개 있는 경우, 
-> activeTasksPercent값은 100f이고,  completedTasksPercent은 0f 이여야 함

 

Tast 2 : getActiveAndCompletedStats_noActive_returnsZeroHundred()

completedTasks이 1개 있고, activeTasks가 없는 경우, 
-> activeTasksPercent값은 0f이고,  completedTasksPercent은 100f 이여야 함

 

Tast 3 : getActiveAndCompletedStats_both_returnsFortySixty()

completedTasks이 2개이고, activeTasks이 3개 있는 경우, 
-> activeTasksPercent값은 40f이고,  completedTasksPercent은 60f 이여야 함

 

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
        val tasks = listOf(
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed with an active task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 100 and 0
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
        val tasks = listOf(
            Task("title", "desc", isCompleted = true)
        )
        // When the list of tasks is computed with a completed task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 0 and 100
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(100f))
    }

    @Test
    fun getActiveAndCompletedStats_both_returnsFortySixty() {
        // Given 3 completed tasks and 2 active tasks
        val tasks = listOf(
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = false),
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed
        val result = getActiveAndCompletedStats(tasks)

        // Then the result is 40-60
        assertThat(result.activeTasksPercent, `is`(40f))
        assertThat(result.completedTasksPercent, `is`(60f))
    }

 

 

2 단계. 버그 테스트 작성

버그 수정을 먼저 하는 대신 먼저 버그가 발생하는 테스트를 작성합니다. 

'버그 테스트'를 작성을 해놔야 나중에 이러한 버그를 실수로 다시 도입하지 않게되고, 이러한 버그가 다시 발생했는지 확인할 수 있게됩니다.

 

Tast 4(버그 테스트) : getActiveAndCompletedStats_error_returnsZeros()

Tasks 에 null 값이 입력되는 경우
-> activeTasksPercent값은 0f이고,  completedTasksPercent은 0f 이여야 함

 

Tast 5(버그 테스트) : getActiveAndCompletedStats_empty_returnsZeros()

Tasks 즉, List<Task>가  emptyList 인 경우
-> activeTasksPercent값은 0f이고,  completedTasksPercent은 0f 이여야 함

    fun getActiveAndCompletedStats_error_returnsZeros() {
        // When there's an error loading stats
        val result = getActiveAndCompletedStats(null)

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_empty_returnsZeros() {
        // When there are no tasks
        val result = getActiveAndCompletedStats(emptyList())

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

 

3 단계. 버그 수정

테스트가 완료되었으니 이제 실제 수행코드의 버그를 수정합니다. 
Tast 4, Task5가 통과되도록하는 수행코드를 수정합니다. 

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

    return if (tasks == null || tasks.isEmpty()) {		// 버그 테스트로부터 추가된 코드
        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
        )
    }
}

 

그 다음 테스트를 다시 실행하고 테스트가 모두 통과되는지 확인합니다. 

 

정리 : TDD를 따르고 테스트를 먼저 작성함으로써 이점

  1. 새로운 기능에는 항상 관련 테스트가 있습니다. 따라서 테스트는 코드수행하는 작업에 대한 문서 역할을합니다.
  2. 테스트는 올바른 결과를 확인하고 이미 본 버그로부터 보호합니다.

 

참고

[CodeLabs] Advanced Android in Kotlin 05.1: Testing Basics을 기반으로 작성

728x90
반응형