Kotest 기본 작성법(Writing tests)

Kotest의 기본 작성법, 테스트 실행과 보고서에 대해 설명한다.

테스트 코드 작성법

Kotest 설치와 설정이 완료되었다면, 테스트 코드의 작성법에 대해서 알아보겠다.

Kotlin에서 제공되는 언어 기능을 사용하면 테스트를 정의하는 데 더욱 강력하면서도 간단한 접근 방식을 제공할 수 있다. 테스트가 Java 파일에 메서드로 정의되어야 하는 시대는 지났다.

Kotest에서 테스트는 본질적으로 테스트 로직이 포함된 TestContext -> Unit 함수일 뿐이다. 이 함수에서 호출된 모든 어설트 문(Kotest 명명법의 Matchers)은 예외를 발생시키면 프레임워크에서 가로채서 해당 테스트를 실패 또는 성공으로 표시하는 데 사용된다.

테스트 함수는 수동으로 정의하는 것이 아니라 이러한 함수를 생성하고 중첩할 수 있는 여러 가지 방법을 제공하는 Kotest DSL을 사용하여 정의한다. DSL은 특정 테스트 스타일을 구현하는 클래스에서 확장되는 클래스를 생성하여 작성한다.

원하는 경로에 테스트 클래스로 생성한 후에 테스트 스타일인 Spec 클래스를 상속 받아야 한다.

예를 들어, Fun Spec 스타일을 사용하려면 FunSpec 클래스를 상속 받고, test 키워드와 이름, 실제 테스트 함수를 사용하여 테스트 함수를 만든다.

class MyFirstTestClass : FunSpec({

   test("my first test") {
      1 + 2 shouldBe 3
   }

})

테스트 코드를 내부를 작성하는 방법은 두 가지가 있는데, 두 가지 주요 방법으로 DSL(Domain-Specific Language)로 작성하는 방법과 init {} 블록을 사용하는 방법이 있다.

이 두가지 방법에 대해서 설명한다.

DSL으로 작성

먼저 DSL(Domain-Specific Language)로 작성하는 방법에 대해서 설명한다.

Kotest는 테스트를 작성하기 위한 DSL을 제공한다. 이 DSL을 사용하면 테스트를 보다 자연스럽게 표현할 수 있다.
각 테스트 스타일에 해당하는 DSL이 제공되며, 이를 사용하여 테스트를 구성할 수 있다. 예를 들어, StringSpec, WordSpec, FunSpec 등은 각각 문자열, 단어, 함수를 사용하여 테스트를 정의하는 DSL을 제공한다.

이러한 DSL은 테스트를 구조화하고 표현하는 데 있어 편리하며, 가독성과 유지보수성을 향상시킨다.

import io.kotest.core.spec.style.StringSpec

class MyStringSpec : StringSpec({
    "This is a test" {
        // 테스트 코드 작성
    }
})

init 블록을 지정하여 작성

Kotlin의 init 블록을 사용하여 테스트를 초기화하고 설정할 수 있다. 이는 일반적으로 더 많은 유연성이 필요한 경우에 사용된다.

init 블록은 테스트 클래스의 생성자 내부에 위치하며, 클래스가 인스턴스화될 때 초기화된다. 따라서 테스트 클래스의 모든 메서드에서 이를 공유할 수 있다.

import io.kotest.core.spec.style.StringSpec

class MyStringSpec : StringSpec() {
    init {
        "This is a test" {
            // 테스트 코드 작성
        }
    }
}

DSL과 init 블록은 각각 다른 사용 사례 및 개발자의 선호도에 따라 선택된다. 보다 구조화된 테스트를 작성하고 싶은 경우 DSL을 사용할 수 있으며, 특정 설정이 필요한 경우 init 블록을 사용할 수 있다.

중첩된 테스트

대부분의 스타일은 테스트를 중첩하는 기능을 제공한다. 실제 구문은 스타일마다 다르지만 기본적으로 외부 테스트에 사용되는 키워드가 다를 뿐이다.

예를 들어, Describe Spec에서 외부 테스트는 describe 함수를 사용하여 만들고 내부 테스트는 it 함수를 사용하여 만든다. JavaScript 및 Ruby 개발자는 해당 언어의 테스트 프레임워크에서 일반적으로 사용되므로 이 스타일을 즉시 알아볼 수 있다.

class NestedTestExamples : DescribeSpec({

   describe("an outer test") {

      it("an inner test") {
        1 + 2 shouldBe 3
      }

      it("an inner test too!") {
        3 + 4 shouldBe 7
      }
   }

})

Kotest 명명법에서 다른 테스트를 포함할 수 있는 테스트를 테스트 컨테이너(test containers)라고 하고, 터미널 또는 리프 노드인 테스트를 테스트 케이스(test cases)라고 한다. 둘 다 테스트 로직과 어설션을 포함할 수 있다.

동적 테스트

테스트는 함수에 불과하므로 런타임에 검증된다.

이 접근 방식은 테스트를 동적으로 생성할 수 있다는 큰 이점을 제공한다. 테스트가 항상 메서드이므로 컴파일 시점에 선언되는 기존 JVM 테스트 프레임워크와 달리, Kotest는 런타임에 조건부로 테스트를 추가할 수 있다.

예를 들어, 목록의 요소를 기반으로 테스트를 추가할 수 있다.

class DynamicTests : FunSpec({

    listOf(
      "sam",
      "pam",
      "tim",
    ).forEach {
       test("$it should be a three letter name") {
           it.shouldHaveLength(3)
       }
    }
})

이렇게 하면 런타임에 3개의 테스트가 생성된다. 다음과 같이 작성하는 것과 동일하다:

class DynamicTests : FunSpec({

   test("sam should be a three letter name") {
      "sam".shouldHaveLength(3)
   }

   test("pam should be a three letter name") {
      "pam".shouldHaveLength(3)
   }

   test("tim should be a three letter name") {
     "tim".shouldHaveLength(3)
   }
})

수명 주기 콜백

Kotest는 테스트 수명 주기 동안 다양한 지점에서 호출되는 여러 콜백을 제공한다. 이러한 콜백은 상태 재설정, 테스트에서 사용할 수 있는 리소스 설정 및 해제 등에 유용하다.

앞서 언급했듯이 Kotest의 테스트 함수는 테스트 컨테이너 또는 테스트 케이스로 레이블이 지정되며, 포함 클래스는 스펙으로 레이블이 지정된다. 테스트 함수, 컨테이너, 테스트 케이스 또는 스펙 자체의 전후에 호출되는 콜백을 등록할 수 있다.

콜백을 등록하려면 콜백 메서드 중 하나에 함수를 전달하기만 하면 된다.

예를 들어, 함수 리터럴을 사용하여 테스트 케이스 전후에 콜백을 추가할 수 있다:

class Callbacks : FunSpec({

   beforeEach {
      println("Hello from $it")
   }

   test("sam should be a three letter name") {
      "sam".shouldHaveLength(3)
   }

   afterEach {
      println("Goodbye from $it")
   }
})

파일에서 콜백의 순서는 중요하지 않다. 예를 들어, 원하는 경우 afterEach 블록을 클래스에서 가장 먼저 배치할 수 있다.

공통 코드를 추출하려면 명명된 함수를 만들어 여러 파일에 재사용할 수 있다. 예를 들어 두 개 이상의 파일에서 모든 테스트 전에 데이터베이스를 재설정하고 싶다면 이렇게 할 수 있다:

val resetDatabase: BeforeTest = {
  // truncate all tables here
}

class ReusableCallbacks : FunSpec({

   beforeTest(resetDatabase)

   test("this test will have a sparkling clean database!") {
       // test logic here
   }
})

모든 콜백에 대한 자세한 내용과 콜백이 호출되는 시기는 Lifecycle HooksExtensions를 참조한다.

Assertion과 Matchers

Kotest는 다양한 Matchers를 제공하여 테스트 결과를 검증할 수 있다. 이를 통해 예상 결과와 실제 결과를 비교하여 테스트가 성공했는지 확인할 수 있다. 예를 들어, 위의 예제에서 result shouldBe 2 부분은 result가 2와 같은지를 검증하는 Assertion이다.

그 밖에 여러 Matchers를 존재하는데, 아래는 Kotest를 사용하여 주요 Matchers를 사용하는 예제 코드이다.

package com.devkuma.kotest.tutorial.assertions

import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldNotContain
import io.kotest.matchers.ints.shouldBeGreaterThan
import io.kotest.matchers.ints.shouldBeInRange
import io.kotest.matchers.ints.shouldBeLessThan
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldBeBlank
import io.kotest.matchers.string.shouldBeEqualIgnoringCase

class MatchersTest : FreeSpec({

    "Matchers" - {
        val number = 10
        "값이 기대한 값과 동일한지 확인한다." {
            number shouldBe 10
        }

        "값이 기대한 값과 다른지 확인한다." {
            number shouldNotBe 20
        }

        val blankString = ""
        "문자열이 공백으로만 구성되어 있는지 확인한다." {
            blankString.shouldBeBlank()
        }

        val numbers = listOf(1, 2, 3, 4, 5)

        "컬렉션이 지정된 요소를 포함하는지 확인한다." {
            numbers shouldContain 3
        }

        "컬렉션이 지정된 요소를 포함하지 않는지 확인한다." {
            numbers shouldNotContain 6
        }

        "컬렉션이 지정된 모든 요소를 포함하는지 확인한다." {
            numbers shouldContainAll listOf(2, 4)
        }

        "컬렉션이 지정된 요소들로 완전히 일치하는지 확인한다." {
            numbers.shouldContainExactly(1, 2, 3, 4, 5)
        }

        val value = 10
        "값이 지정된 범위에 속하는지 확인한다." {
            value shouldBeInRange 5..15
        }

        "값이 지정된 값보다 큰지 확인한다." {
            value shouldBeGreaterThan 5
        }

        "값이 지정된 값보다 작은지 확인한다." {
            value shouldBeLessThan 15
        }

        val message = "Hello, Kotest"
        "대소문자 무시하고 일치하는지" {
            message shouldBeEqualIgnoringCase "HELLO, KOTEST"
        }
    }
})

이 외에 더 많은 Matchers를 존재를 하고, 상세한건 다시 설명하도록 하겠다.

테스트 실행과 보고서

테스트를 실행하려면 IDE에서 테스트를 실행하거나, Gradle 또는 Maven을 사용하여 테스트를 실행할 수 있다. 테스트 실행 후에는 테스트 결과에 대한 보고서를 확인할 수 있다. Kotest는 테스트 결과를 자세하게 기록하고 보고서를 생성하여 개발자가 테스트의 상태를 파악할 수 있도록 도와준다.

테스트를 실행하려면 IDE에서 해당 테스트 클래스나 파일을 실행하거나, Gradle 또는 Maven을 사용하여 테스트를 실행할 수 있다. 테스트 실행 후에는 각 테스트 케이스의 결과를 보고서 형태로 확인할 수 있다. Kotest는 실행된 테스트에 대한 결과를 자세하게 보고서로 제공하여 테스트의 성공 또는 실패를 파악할 수 있도록 한다.

IntelliJ IDEA 에서 테스트 실행

IntelliJ IDEA 에서 테스트 클래스 및 파일을 선택하고, 왼쪽에 kotest 탭을 선택하면, 아래와 같이 테스트 케이스가 표시가 된다.
IntelliJ IDEA 실행

표시가 된 테스트 케이스를 선택한 상태에서 Run 버튼을 누르면, 아래와 같이 테스트 코드가 실행되는 것을 확인할 수 있다.
IntelliJ IDEA 실행 결과

Gradle 테스트 실행

Kotest를 Gradle을 통해 실행하는 방법은 다음과 같다.

테스트 명령 실행

터미널 및 커멘트 창에서 프로젝트 디렉터리로 이동한 후 다음 명령을 실행하여 Gradle을 사용하여 프로젝트의 테스트를 실행한다.

./gradlew test

실행이 완료되면 테스트 결과가 터미널에 출력된다. 아래는 테스트가 통과되면 BUILD SUCCESSFUL 메세지가 출력된다.

% ./gradlew test

BUILD SUCCESSFUL in 548ms
3 actionable tasks: 3 up-to-date

실패한 테스트가 발생하면, BUILD FAILED 메세지가 출력된다.

 % ./gradlew test 

BUILD SUCCESSFUL in 28s
3 actionable tasks: 3 executed
user@AL02263852 kotest-tutorial % ./gradlew test

> Task :test

com.devkuma.kotest.tutorial.MyTest > My first test FAILED
    io.kotest.assertions.AssertionFailedError at MyTest.kt:9

58 tests completed, 1 failed, 8 skipped

> Task :test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/user/develop/kotlin-tutorial/kotest-tutorial/build/reports/tests/test/index.html

* Try:
> Run with --scan to get full insights.

BUILD FAILED in 26s
3 actionable tasks: 2 executed, 1 up-to-date

테스트 보고서 확인

테스트 실행 후 build/reports/tests 디렉터리에 생성된 HTML 형식의 테스트 보고서를 확인하여 테스트 결과를 자세히 볼 수 있다.

위에서 실패한 경우에 출력된 내용을 보면 테스트 보고서 링크가 아래와 같이 표시된다.

> There were failing tests. See the report at: file:///Users/user/develop/kotlin-tutorial/kotest-tutorial/build/reports/tests/test/index.html

이 파일을 브라우저로 열면 아래와 같이 표시된다.
Test Summary 실패

실행 테스트가 몇건인지, 실패한 건수는 몇건인지 상세하게 표시된다.

이는 성공한 경우에도 명령 출력만 되지 않을 뿐이지, 동일한 파일 위치에 파일이 생성이 된다.

Test Summary 성공


이렇듯 Kotest를 사용하면 간단한 설정과 테스트 코드 작성으로 쉽고 효율적으로 테스트를 수행할 수 있다. 다음 섹션에서는 더 많은 Kotest의 기능과 활용법에 대해 다루어 보겠다.


참조




최종 수정 : 2024-04-21