3 minute read

Kotest

새로 들어가게된 프로젝트가 코틀린으로 되어있어서, 해당 프로젝트에서 사용중인 kotest를 알아보기 위해 정리를 해보자.

Kotest.io 공식 사이트


kotest 들어가기 전에

  • gradle dependencies 추가
    dependencies {
      .
      .
      .
    
      testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
      testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
    }
    
  • intellij plugin 설치
    인텔리제이를 사용한다면 Kotest 플러그인을 설치해 주자

    Preference → Plugins → ‘Kotest’ 검색 → 설치


Test Style

kotest에는 테스트 레이아웃이 10개 있는데 이중에 하나를 상속받아 진행한다. 여러 테스트 프레임워크에서 영향을 받아 만들어진 것도 있고, 코틀린만을 위해 만들어진 것도 있다.

Testing Styles

어떤 스타일을 고르던 기능적 차이는 없다. 취향에 따라, 팀 또는 개인의 스타일에 따라 고르면 될 듯 하다.

  • ex) FreeSpec으로 하려고 한다면
internal class HumanTest : FreeSpec() {

}

아래부터 예제코드는 FreeSpec 기준으로 작성함.


전후 처리

기존 @BeforeEach, @BeforeAll, @AfterEach 등과 같은 전후처리를 위한 기본 어노테이션을 사용하지않고 각 Spec의 SpecFunctionCallbacks 인터페이스에 의해 override를 하여 구현 할 수 있다.

interface SpecFunctionCallbacks {
   fun beforeSpec(spec: Spec) {}
   fun afterSpec(spec: Spec) {}
   fun beforeTest(testCase: TestCase) {}
   fun afterTest(testCase: TestCase, result: TestResult) {}
   fun beforeContainer(testCase: TestCase) {}
   fun afterContainer(testCase: TestCase, result: TestResult) {}
   fun beforeEach(testCase: TestCase) {}
   fun afterEach(testCase: TestCase, result: TestResult) {}
   fun beforeAny(testCase: TestCase) {}
   fun afterAny(testCase: TestCase, result: TestResult) {}
}

위 인터페이스를 참고하여 작성해보면 아래와 같이 사용 할 수 있다.

internal class HumanTest : FreeSpec() {

    override fun beforeSpec(spec: Spec) {
        println("beforeSpec")
    }

    override fun beforeTest(testCase: TestCase) {
        println("beforeTest")
    }

    override fun beforeContainer(testCase: TestCase) {
        println("beforeContainer")
    }

    override fun beforeEach(testCase: TestCase) {
        println("beforeEach")
    }

    override fun beforeAny(testCase: TestCase) {
        println("beforeAny")
    }

    init {
        "그냥 컨테이너" - {
            "그냥 테스트1" {
                println("그냥 테스트1")
                "".length shouldBe 0
            }
            "그냥 테스트2" {
                println("그냥 테스트2")
                "12345".length shouldBe 5
            }
        }
    }
}

결과

실행결과

beforeSpec

beforeContainer
beforeAny
beforeTest

beforeEach
beforeAny
beforeTest
그냥 테스트1

beforeEach
beforeAny
beforeTest
그냥 테스트2

결과를 보면 각 fun들이 어느시점에 실행되는지 확인 가능하다.

AnnotationSpec 을 사용하면 아래와 같이 사용도 가능하다.

internal class HumanTest : AnnotationSpec() {

    @BeforeEach
    fun beforeTest() {
        println("Before each test")
    }
    
    init{
    }
}

Assertion 알아보기

kotest는 아주 풍부한 assertion을 제공하는데, 몇가지 assertion 사용법에 대해 알아보자.

Assertions

assertion을 다 알아보기에는 너무 많으니 예제로 대체한다.

init {
    "Matchers" - {
        val testStr = "I am iron man"
        val testNum = 5
        val testList = listOf<String>("iron", "bronze", "silver")

        "일치 하는지" {
            testStr shouldBe "I am iron man"
        }
        "일치 안 하는지" {
            testStr shouldNotBe "I am silver man"
        }
        "해당 문자열로 시작하는지" {
            testStr shouldStartWith "I am"
        }
        "해당 문자열을 포함하는지" {
            testStr shouldContain "iron"
        }
        "리스트에서 해당 리스트의 값들이 모두 포함되는지" {
            testList shouldContainAll listOf("iron", "silver")
        }
        "대소문자 무시하고 일치하는지" {
            testStr shouldBeEqualIgnoringCase "I AM IRON MAN"
        }
        "보다 큰거나 같은지" {
            testNum shouldBeGreaterThanOrEqualTo 3
        }
        "해당 문자열과 길이가 같은지" {
            testStr shouldHaveSameLengthAs "I AM SUPERMAN"
        }
        "문자열 길이" {
            testStr shouldHaveLength 13
        }
        "여러개 체이닝" {
            testStr.shouldStartWith("I").shouldHaveLength(13).shouldContainIgnoringCase("IRON")
        }
    }
}

Exception 발생하는지도 체크 가능하다.

"Exception" - {
    "ArithmeticException Exception 발생하는지" {
        val exception = shouldThrow<ArithmeticException> {
            1 / 0
        }
        exception.message shouldStartWith("/ by zero")
    }
    "어떤 Exception이든 발생하는지" {
        val exception = shouldThrowAny {
            1 / 0
        }
        exception.message shouldStartWith("/ by zero")
    }
}

Clues를 이용해서 에러메세지에 실마리?를 남길 수 도 있다.

"Clues" - {
    data class HttpResponse(val status: Int, val body: String)
    val response = HttpResponse(404, "the content")
    
    "Not Use Clues" {
        response.status shouldBe 200
        response.body shouldBe "the content"
        // 결과: expected:<200> but was:<404>
    }
    "With Clues" {
        withClue("status는 200이여야 되고 body는 'the content'여야 한다") {
            response.status shouldBe 200
            response.body shouldBe "the content"
        }
        // 결과: status는 200이여야 되고 body는 'the content'여야 한다
    }
    "As Clues" {
        response.asClue {
            it.status shouldBe 200
            it.body shouldBe "the content"
        }
        // 결과: HttpResponse(status=404, body=the content)
    }
}

위의 결과(주석) 처럼 test실패 했을 때 더 자세한 단서를 남길 수 있다.

Soft Assertion을 사용하면 중간에 asert가 실패해도 끝까지 체크가 가능하다. assertAll 처럼

"Soft Assertions" - {
    val testStr = "I am iron man"
    val testNum = 5

    "Not Soft" {
        testStr shouldBe "IronMan"
        testNum shouldBe 1
        // 결과: expected:<"IronMan"> but was:<"I am iron man">
    }
    "Use Soft" {
        assertSoftly {
            testStr shouldBe "IronMan"
            testNum shouldBe 1
        }
        // 결과: expected:<"IronMan"> but was:<"I am iron man">
        //      expected:<1> but was:<5>
    }
}

Data Driven Testing

아래 기능을 이용해서 다른 매개변수를 정의하여 각각 테스트가 가능하다.

"data test" - {
    "forAll" {
        forAll(
            row("haha", 13),
            row("hoho", 22),
        ) { name, age ->
            name.length shouldBe 4
            age shouldBeGreaterThanOrEqualTo 10
        }
    }
    "table forAll" {
        table(
            headers("name", "age"),
            row("haha", 13),
            row("hoho", 22)
        ).forAll { name, age ->
            name.length shouldBe 4
            age shouldBeGreaterThanOrEqualTo 10
        }
    }
    "collection" {
        listOf(
            row("haha", 13),
            row("hoho", 22)
        ).map { (name: String, age: Int) ->
            name.length shouldBe 4
            age shouldBeGreaterThanOrEqualTo 10
        }
    }
}

이렇게 데이터를 세팅하고, 각 행 별로 테스트 할 수 있다.


아주 간단한 것 만 해보았는데, 문서를 보면 해보지 않은 여러가지 기능, 장점들(특정 주기로 테스트, Generators를 이용한 속성 기반 테스트, 광범위한 확장성 등)이 많아서 필요할 때 참고해 보는것도 좋을 것 같다.

Comments