Develop/Kotlin

[Kotest] Kotest 활용 간단 가이드

연로그 2024. 10. 27. 16:37
반응형

Kotest란? 🤔

 

공식 사이트에 가보면 Kotest를 아래와 같은 말로 표현하고 있다.

Kotest is a flexible and elegant multi-platform test framework for Kotlin with extensive assertions and integrated property testing

 

위의 말을 보면 총 3가지 정도의 기능을 제공하고 있다. 각 기능들을 독립적으로 사용할 수 있는데, 이는 Kotest가 독립적으로 나뉜 여러 개의 하위 프로젝트들로 구성되어 있기에 가능한 일이다. (3가지 기능을 모두 사용하고 싶다면 의존성도 각자 추가해야 한다는 의미이다.) 일단 어떤 기능들을 제공하고 있는지부터 살펴보자.

 

1. 유연하고 우아한 멀티 플랫폼 테스트 프레임워크다.

👉 멀티 플랫폼이란 다양한 종류의 플랫폼에서 동작할 수 있다는 것을 의미한다. Kotest를 사용하면 JVM, JavaScript, Native와 같은 여러 플랫폼에서 동일한 테스트 코드를 실행할 수 있다.

 

2. 다양한 검증 방법(assertions)이 존재한다.

👉 어떤 상태를 테스트할 수 있는 함수를 제공한다. 내가 함수의 수행 결과가 내가 기대한 값과 일치하는지, 예외가 발생하는지 등을 검증하기 위한 함수를 제공하고, 이 검증 과정을 그룹화하기 위한 유틸리티 등 다양한 기능을 제공한다.

 

3. 통합된 속성 테스팅(property testing)를 제공한다.

👉 속성 기반 테스트(property based test)는 주어진 조건이나 속성을 만족하는지 다양한 입력값으로 자동으로 테스트하는 방식이다. 예를 들어, 정수를 입력받아 0인지 아닌지 판단하는 함수가 있다고 가정해 보자. 이 함수에 입력할 정수 값들을 자동으로 생성해서 테스트할 수 있는 방식이 속성 기반 테스트이다. 아래는 Kotest의 forAll를 이용해 작성한 예제 코드다.

class StringSpecTest : StringSpec({
    "forAll test" {
        forAll<Int> { num ->
            println(num)
            notZero(num) == (num != 0)
        }
    }
})

fun notZero(num: Int) = num != 0

엄청난 양의 입력 값을 자동으로 생성해주었다.

 

 


Kotest 시작하기 😄

 

이 글에서는 assertion이나 property testing의 기능은 제하고 테스트 프레임워크로서의 Kotest만 다뤄볼 예정이다. 만약 assertion, property testing의 기능도 테스트하고 싶다면 Kotest 공식 문서를 참고해 별도의 의존성을 추가하길 바란다.

 

Gradle + Kotlin을 사용하는 경우, 아래 의존성을 추가한다.

dependencies {
    // ...
    testImplementation 'io.kotest:kotest-runner-junit5:$version'
}

tasks.withType<Test>().configureEach {
   useJUnitPlatform()
}

 

IntelliJ를 사용하는 경우, 플러그인을 설치하면 각 테스트에 대한 실행 아이콘이나 중복된 테스트가 작성되면 하이라이트로 표시하는 등 다양한 기능을 제공한다.

Intellij Settings 창

 


Kotest 테스트 스타일 🥸

Kotest는 10가지 방식의 테스트 레이아웃 스타일(styles of test layout)을 제공한다.

간단한 예제를 통해 활용 방법을 알아보고, 어떤 경우에 활용하면 좋을지 고민해보자.

 

🔸 StringSpec

  • 가장 심플한 형태의 테스트
class StringSpecTest : StringSpec({
    "글자수 검증" {
        "hello".length shouldBe 5
    }
})

 

 

🔸 FunSpec

  • test()를 통해 테스트 생성 가능
  • 앞에 x-를 붙임으로써 테스트 대상에서 제외 가능
  • context()를 통해 각 테스트끼리의 공유 범위를 지정 가능
class FunSpecTest : FunSpec({
    test("글자수 검증") {
        "hello".length shouldBe 5
    }

    // xtest는 실행되지 않는다.
    xtest("실행되지 않는 테스트") {
        "hello".length shouldBe 3
    }

    // context()를 설정하여 변수를 공유하게 만들 수 있다.
    context("문자열의") {
        val string = "hello"

        test("글자수가 5이다") {
            string.length shouldBe 5
        }

        test("첫 글자는 h이다") {
            string.first() shouldBe 'h'
        }
    }
})

전체 테스트 실행 결과 화면

 

🔸 ShouldSpec

  • FunSpec과 유사
class ShouldSpecTest : ShouldSpec({
    should("hello의 글자수는 5이다") {
        "hello".length shouldBe 5
    }

    // xshould는 실행되지 않는다.
    xshould("실행되지 않는 테스트 케이스") {
        "hello".length shouldBe 1
    }

    // context()를 설정하여 변수를 공유하게 만들 수 있다.
    context("context") {
        val string = "hello"
        should("hello의 글자수는 5이다") {
            string.length shouldBe 5
        }

        should("hello의 첫 글자는 h이다") {
            string.first() shouldBe 'h'
        }
    }
})

전체 테스트 실행 결과 화면

 

🔸 BehaviorSpec

  • given - when - then 형식으로 표기가 가능한 형식
  • 단, when은 코틀린에서 키워드로 사용되므로 ` 을 붙여야 함
class BehaviorSpecTest : BehaviorSpec({
    given("문자열에서") {
        val string = "hello"

        `when`("글자수를 확인하면") {
            val result = string.length

            then("글자수는 5이다") {
                result shouldBe 5
            }
        }
    }
})

 

🔸 AnnotationSpec

  • JUnit과 유사한 형식으로, 어노테이션을 활용
  • @BeforeAll, @BeforeEach, @AfterAll, @AfterEach 등의 어노테이션도 활용 가능
class AnnotationSpecTest : AnnotationSpec() {
    @Test
    fun 글자수_검증() {
        "hello".length shouldBe 5
    }
}

 

💡 어떤 스타일을 활용하는 것이 좋을까?

위에서 소개한 Spec 외에도 FreeSpec, WordSpec, FeatureSpec, ExpectSpec 등 다양한 스타일이 존재한다. 각자 뚜렷한 개성을 가진 Spec도 있지만 이름만 다를 뿐 기능에는 큰 차이가 없는 Spec들도 있다.

 

활용 예제를 살펴보자. JUnit에서 Kotest로 옮긴다면 AnnotationSpec을 활용하는 게 마이그레이션 하기 훨씬 간편하다. 간단한 유틸성 메서드 테스트는 StringSpec을 활용해 코드를 최소화할 수 있다. given - when - then 활용한 방식의 테스트를 선호한다면 BehaviorSpec이 익숙할 수도 있다.

 

이중 어떤 것을 활용하면 좋을지에 대한 정답은 없다. 어떤 팀에서는 하나의 스타일로 통일하는 것을 좋아할 수도 있고, 어떤 팀에서는 다양하게 사용하는 것을 선호할 수도 있다. 각 팀의 상황에 맞게 컨벤션을 맞춰나가면 좋을 것 같다.

 

 


Kotest 활용하기

 

Lifecycle Hooks

테스트 실행 전/후로 공통적으로 처리하고 싶은 코드들이 있을 수 있다. 이 경우, beforeTest / afterTest를 활용할 수 있다.

class ShouldSpecTest : ShouldSpec({
    // 각 테스트 수행 전마다 수행
    beforeTest {
        println("테스트 시작")
    }

    // 각 테스트 수행 후마다 수행
    afterTest { 
        println("테스트 종료")
    }
    
    should("hello의 글자수는 5이다") {
        "hello".length shouldBe 5
    }
}

 

이 외에도 생명주기(lifecycle)에 따라 다양한 메서드를 제공하는데 Kotest 공식 문서 - Lifeycycle Hooks를 참고 바란다. 

 

Lifecycle hooks | Kotest

It is extremely common in tests to want to perform some action before and after a test, or before and after all tests in the same file.

kotest.io

 

모킹

Kotest 자체에서는 모킹을 위한 라이브러리가 제공되지 않는다. MockK과 같은 별도의 라이브러리를 활용해야 한다.

 

MockK

Provides DSL to mock behavior. Built from zero to fit Kotlin language. Supports named parameters, object mocks, coroutines and extension function mocking

mockk.io

 


참고

반응형