Kotest로 해보는 안드로이드 테스트 (상)
728x90
반응형

이미지 출처 : https://ramadandev.medium.com/test-driven-development-for-android-developers-ceb55ae3ea16

 

테스트 코드를 만들어 본 적은 있지만 안드로이드 프로젝트 내에 테스트 코드를 적용해 본 적이 없었다. 그래서 TDD같은 단어는 그림의 떡. 남들이 말하는 유닛 테스트는 "우와 멋있는 말"이라고 감탄할 뿐이었다. 그렇다고 언제까지 감탄만 할 것인가. 부러워만 하지 말고 이제 테스트 코드를 넣어보자. 하다못해 넣는 척이라도.

 

(권장사항 : Junit으로 테스트하는 방법을, 간단하게라도 알고 오면 해당 내용이 더 쉬울 수 있다. Junit을 사용하는 방법만 알고 본 내용을 읽어도 괜찮다. 본 내용은 코틀린으로 진행하지만 많은 샘플을 찾아 볼 수 있는 Java로라도 Junit을 사용하는 방법을 알아보자.)

 

 

지겹도록 보지만 제대로 이해하고 보는 사람은 몇이나 될까. (일단 난 아님)

 

어디서부터 접근을 해야할까?

테스트를 할 포인트를 잡아야 할 텐데... 기획자나 디자이너의 요청으로 변경이 잦은 뷰는 로직이 변경될 때마다 테스트 코드도 다시 짜야하는 번거로움이 생긴다. 그래서 강남언니 공식 블로그에서는 도메인 로직쪽에 포커싱을 맞춘 테스트를 보여주었다. 프리젠테이션 레이어와 독립적으로 구성되어 있으며 뷰의 변경으로부터 자유롭기 때문에 도메인 로직 위주의 테스트 코드를 작성해보려고 한다. 필자가 테스트하려고 하는 프로젝트에는 Use-case는 없다. 그래서 Entity를 포커싱으로 한 테스트를 만들어보려고 한다.

 

그런데 앞의 강남언니 공식 블로그의 글을 읽다보면 TDD와 DDD라는 것이 등장한다. 특히 도메인에 포커싱을 맞출수록 DDD(Domain Driven Design)라는 것이 궁금해진다. 안드로이드 개발자다보니 TDD보다 생소한 개념이다. 게다가 도메인(Domain)이라는 단어조차도 처음 "객체"라는 단어를 만났을 때만큼이나 모호한 기분이다. 이 부분은 가능하면 강남언니 공식 블로그와 Pluu님의 DroidKaigi 2017 번역글(👍추천)을 참고하는 것이 나을 것 같다.

 

그렇다고 도메인을 만드는 것부터 하기엔 조금 부담스럽다. 이미 만들어진 도메인 모델이 있으니 일단 이것으로 테스트를 다루어보자. 테스트를 하기 위해 여러 테스팅 도구(혹은 테스트 프레임워크)를 고려하게 된다. 필자는 강남 언니 블로그에서 소개된 Kotest를 사용하여 테스트를 해보려고 한다. Kotest가 Kotlin 코드에 맞춰 테스트를 할 수 있게 만들어진 도구라서 안드로이드 뿐만 아니라 Kotlin-Spring에서도 사용이 가능하다. Kotest가 다른 테스트 도구들과 어떻게 다른지 궁금하다면 Veluexer's Blog의 글을 참고하자.

 

여~ Kotest. 만나서 반갑고~

 

Kotest를 알아가다 보면 Data driven testing이라는 말을 자주 보게 된다. 데이터 기반 테스팅. 보통 Junit으로 테스트를 할 때, 특정 시나리오를 바탕으로 "제시된 예제가 제대로 실행되는가"를 검증한다고 보면된다. 그런데 이렇게 테스트 코드를 작성할 경우, 다수의 예제를 검증하고 싶을 때, 코드가 비대해지면서 가독성이 떨어지는 단점이 나타난다. 이런 점을 보완한 것이 데이터 기반 테스팅이고 Kotest는 이 테스팅으로 코드를 검증하고 있다. Data driven testing는 제공한 입력값을 바탕으로 테스트 케이스 항목이 자동으로 생성된다.

 

Kotest 공부하면서 Data driven testing와 함께 자주 볼 수 있는 단어가 바로 BDD(Behavior Driven Development)이다. 행위 주도 개발. BDD는 시나리오 기반의 테스트를 진행한다. 이 시나리오 진행에 필요한 값을 설정하고(Given) 시나리오 진행에 필요한 조건을 명시하고(When) 시나리오 완료시 보장해야 하는 결과 값을 명시해야(Then) 한다. 또 다른 개발 방법론인 TDD와 비교하는 글을 본다면 더 이해하기 쉬울 것이다.

 

 

 

 

개념 설명은 여기까지. Kotest를 사용해볼까.

Kotest를 사용하기 위한 준비

먼저 Kotest의 Kotest test framework를 세팅해보자. Kotest 공식 홈페이지에 Quick Start로 가이드 되어 있지만 안드로이드 기준으로 어떻게 세팅했는지 다시 정리해 보겠다.

 

안드로이드의 Kotest는 JUnit Platform gradle 플러그인을 사용한다. app 단위 build.gradle에서 JUnit Platform gradle 플러그인을 사용하기 위한 testOption을 추가해야 한다.

// build.gradle(:app)
android {

	// 일부 생략

    testOptions {
        unitTests.all {
            useJUnitPlatform()
        }
    }
}

그리고 Kotest junit5 runner를 추가한다. (의존성 설정)

// build.gradle(:app)
dependencies {
	// 일부 생략

	testImplementation 'io.kotest:kotest-runner-junit5:4.3.2'
}

 

Jetbrains plugin marketplace에서 Kotest라는 이름의 IntelliJ 플러그인까지 설치하면 편리한 테스트 실행이 가능해진다. 여기까지 했다면 Kotest를 사용할 준비가 완료. 정상적으로 작동하는지 궁금하다면 홈페이지에 나오는 샘플 코드를 생성해서 실행시켜보자. 필자가 만든 샘플 코드를 참고해도 된다.

 

과거에 필자는 테스트 자체가 생소해서 어떻게 실행하는지도 몰랐던 적이 있다. 그런 분들을 생각하여 좀 더 상세하게 작성해보고자 한다. 테스트 코드(단위 테스트)는 아래 이미지처럼 반드시 "src/test/java/"에 위치해야 한다. 해당 위치에 샘플코드를 만들면 코드 왼쪽, Line number가 있는 공간에 녹색의 아이콘이 나타나는 것을 볼 수 있다. 이 아이콘을 클릭하면 여러개의 메뉴가 있고 여기서 "Run 테스트파일"로 보이는 가장 상단의 메뉴를 선택하면 테스트가 실행된다.

 

출처 : https://developer.android.com/studio/test
왼쪽 공간에 녹색 실행 아이콘이 보인다. (출처 : https://kotest.io/docs/intellij/intellij-plugin.html#running-tests)

 

 

 

Testing Styles

Kotest는 테스트 레이아웃을 받아서 테스트 코드를 작성한다. 이 레이아웃은 다른 테스트 프레임워크를 고려한 양식인 경우도 있지만 Kotest만의 양식으로 나온 것도 있다. 이 레이아웃을 테스팅 스타일이라고 부른다. 여기서는 일부 스타일만 다뤄보고 본격적인 테스트 실습을 해보려고 한다.

 

테스팅 스타일과 함께 알아둬야 하는 것이 바로 Matcher. JUnit의 assert 메소드와 비슷하다고 생각하면 된다. 공식 홈페이지의 설명에 따르면, 특정 테스트를 수행하는 Assertion(일종의 표현식)에 대한 Kotest 용어이다. 그런데 이 말이 더 어렵다. 오히려 Matcher의 종류를 보면 더 쉽게 이해할 수 있을 것이다. 아래 코드에서 "shouldBe"는 전후가 동일한지 확인하는 Matcher이다. 이렇게 실제 테스트를 하도록 돕는 것이 Matcher이다.

 

class MyTests : FunSpec({
    test("String length should return the length of the string") {
        "sammy".length shouldBe 5
        "".length shouldBe 0
    }
})

class MyTests2 : StringSpec({
    "strings.length should return size of string" {
        "hello".length shouldBe 5
    }
})
  • FunSpec
    • test라는 함수를 사용한다. 함수 인자로 테스트를 설명하는 문자열 인수는 넣는다.
    • 호출한 뒤, 람다를 사용하여 테스트를 만들 수 있다.
  • StringSpec
    • FunSpec을 더 함축한 형태
    • test 함수가 없고 바로 문자열과 람다를 사용한다.

 

class MyTests : ShouldSpec({
    context("String.length") {
        should("return the length of the string") {
            "sammy".length shouldBe 5
            "".length shouldBe 0
        }
    }

    context("sample") {
        val sample = "abcd"
        sample should startWith("ab")
    }
})
  • ShouldSpec
    • FunSpec과 비슷하다. 다만 test 함수가 아닌 should 함수를 쓴다는 점
    • should를 별도의 import 없이 중위호출 형태로 사용이 가능하다. (아래쪽 "sample should ..." 코드)

 

class MyTests4 : BehaviorSpec({
    given("100점이 만점인 상황에서") {
        val totalMarks = 100
        `when`("학생의 점수가") {
            and("90점 이상이라면") {
                val obtainedMarks = 92
                then("등급은 A") {
                    getGrade(obtainedMarks, totalMarks) shouldBe "A"
                }
            }

            and("80점 이상 90점 미만이라면") {
                val obtainedMarks = 88
                then("등급은 B") {
                    getGrade(obtainedMarks, totalMarks) shouldBe "B"
                }
            }

            and("70점 이상 80점 미만이라면") {
                val obtainedMarks = 77
                then("등급은 C") {
                    getGrade(obtainedMarks, totalMarks) shouldBe "C"
                }
            }

            and("60점 이상 70점 미만이라면") {
                val obtainedMarks = 66
                then("등급은 D") {
                    getGrade(obtainedMarks, totalMarks) shouldBe "D"
                }
            }

            and("60점 미만이라면") {
                val obtainedMarks = 34
                then("등급은 F") {
                    getGrade(obtainedMarks, totalMarks) shouldBe "F"
                }
            }
        }
    }
})


fun getGrade(obtainedMarks: Int, totalMarks: Int): String {
    val percentage = getPercentage(obtainedMarks, totalMarks)
    return when {
        percentage >= 90 -> "A"
        percentage in 80..89 -> "B"
        percentage in 70..79 -> "C"
        percentage in 60..69 -> "D"
        else -> "F"
    }
}

private fun getPercentage(obtainedMarks: Int, totalMarks: Int): Int {
    return (obtainedMarks / totalMarks.toFloat() * 100).roundToInt()
}
  • BehaviorSpec
    • 만약 앞에서 설명한 BDD로 개발을 선호한다면 사용하게 될 테스팅 스타일
    • BDD의 기본 패턴인 Given, When, Then 구조를 가지고 와서 이름 그대로의 함수를 이용할 수 있음
      • given : 시나리오 진행에 필요한 값 설정
      • when : 시나리오를 진행하는데 필요한 조건 명시
      • then : 시나리오 완료시 보장해야 하는 결과 값 명시
    • 예시 1 : Painless Unit Testing with Kotlintest & Mockk
    • 예시 2 : harry-jk/ifkakao-2020-code
    • 아래는 실행 결과

위 BehaviorSpec 예시 결과

 

 


 

이렇게 안드로이드 테스트하기 전에 알아두면 좋을 Kotest의 테스팅 스타일을 알아보았다. 이제 진짜 테스트를 해보자. 다음 포스팅에...

 

 

참고 자료
- TDDDD 안드로이드에서 가능할까?
- 안드로이드 테스트
- 카카오헤어샵의 DDD
- Kotest 공식 홈페이지
- Kotlin In Action 11장
- Comparing Testing Library for Kotlin
- Kotlin Unit Test 하는법

언젠가 차분히 보고 싶은 자료
- if(kakao) 2020 세션 : kotest가 있다면 TDD 묻고 BDD로 가!

본 포스팅의 실습 코드가 궁금하신 분들은 필자의 Github를 참고해주세요.

 

728x90
반응형