Kotest 기본 확장(Extensions)

Kotest의 Extensions는 Kotest 테스트 실행 프레임워크에서 테스트 라이프사이클에 통합할 수 있는 다양한 기능을 제공하는 확장 기능이다.

확장 기능

확장(Extensions)은 재사용 가능한 라이프사이클 훅이다. 사실 라이프사이클 훅은 그 자체로 내부적으로 확장의 인스턴스로 표현된다. 과거에는 단순한 인터페이스에는 리스너라는 용어를, 고급 인터페이스에는 확장이라는 용어를 사용했지만, 지금은 둘을 구분하지 않으며 두 용어를 혼용해서 사용할 수 있다.

확장를 활용하면 테스트 실행 도중 추가적인 동작을 수행하거나 사용자 지정 기능을 통합할 수 있다. 확장를 사용하여 테스트를 보다 유연하게 제어하고 확장할 수 있다.

일반적으로 Extensions는 테스트 전/후에 수행되는 작업을 정의하거나, 특정 조건에서 테스트를 비활성화하거나 특정한 방식으로 로깅을 수행하는 등의 작업을 수행할 수 있다. Extensions는 리스너, 인터셉터, 인터페이스 등의 형태로 제공되며, 다양한 시나리오에 대응할 수 있다.

확장 기본 사용법

기본 사용법은 필요한 확장 인터페이스의 구현을 생성하고 이를 테스트, 스펙 또는 프로젝트 전체에 ProjectConfig에 등록하는 것이다.

예를 들어, 아래 예제는 스펙 전후 리스너를 생성하고, 이를 스펙에 등록하고 있다:

package com.devkuma.kotest.tutorial.extensions.ex1

import io.kotest.core.listeners.AfterSpecListener
import io.kotest.core.listeners.BeforeSpecListener
import io.kotest.core.spec.Spec

class MyTestListener : BeforeSpecListener, AfterSpecListener {
    override suspend fun beforeSpec(spec: Spec) {
        println("beforeSpec")
    }
    override suspend fun afterSpec(spec: Spec) {
        println("afterSpec")
    }
}

이 확장 기능을 적용하려면 extension 함수로 아래와 같이 리스너를 지정한다:

package com.devkuma.kotest.tutorial.extensions.ex1

import io.kotest.core.spec.style.WordSpec

class TestSpec : WordSpec({
    extension(MyTestListener())
    
    "testSpec" should {
        println("testSpec")
    }
})

다음은 예제를 실행한 결과이다:

beforeSpec
testSpec
afterSpec

스펙 내에 등록된 모든 확장은 해당 ‘Spec’의 모든 테스트(테스트 팩토리 및 중첩된 테스트 포함)에 사용된다.

전체 프로젝트의 모든 사양에 대해 확장 기능을 적용하려면, @AutoScan 애노테이션을 지정하여 등록할 수 있다.

아래 예제는 전후 리스너를 생성하여 @AutoScan를 지정하면 모든 스펙에 적용된다:

package com.devkuma.kotest.tutorial.extensions.ex2

import io.kotest.core.listeners.AfterProjectListener
import io.kotest.core.listeners.BeforeProjectListener

@AutoScan
class ProjectListener : BeforeProjectListener, AfterProjectListener {
    override suspend fun beforeProject() {
        println("beforeProject")
    }

    override suspend fun afterProject() {
        println("afterProject")
    }
}

아래 예제 코드에서는 앞에 예제와 다르게 따로 확장을 지정하지 않았다:

package com.devkuma.kotest.tutorial.extensions.ex2

import io.kotest.core.spec.style.FunSpec

class ProjectTest1 : FunSpec({
    test("test1") {
        println("test1")
    }
})

class ProjectTest2 : FunSpec({
    test("test2") {
        println("test2")
    }
})

다음은 예제를 실행한 결과이다:

beforeProject
test1
test2
afterProject

간단한 확장

간단한 확장(Simple Extensions)는 Kotest에서 다양한 리스너를 제공하여 테스트 실행 전후에 사용자가 원하는 작업을 수행할 수 있다.

아래 테이블에서는 테스트 및 사양 수명 주기 이벤트를 다루는 가장 기본적인 확장이 나열되어 있으며 대부분 수명 주기 훅과 동일하다. 엔진 실행 방식을 수정하는 데 사용할 수 있는 고급 확장에 대해서는 고급 확장을 참조하라.

각 리스너의 역할과 사용법은 아래와 같다:

확장 설명
BeforeContainerListener 컨테이너(테스트 그룹)가 실행되기 전에 호출된다.
주로 컨테이너 전체를 설정하는 작업에 사용된다. 예를 들어, 특정 데이터베이스를 초기화하거나, 외부 리소스를 설정하는 등의 작업을 수행할 수 있다.
AfterContainerListener 컨테이너(테스트 그룹)가 실행된 후에 호출된다.
주로 컨테이너 이후에 필요한 정리 작업에 사용된다. 예를 들어, 데이터베이스 연결을 닫거나, 외부 리소스를 해제하는 등의 작업을 수행할 수 있다.
BeforeEachListener 각 테스트가 실행되기 전에 호출된다.
각 테스트 실행 전에 공통적으로 필요한 설정 작업을 수행할 수 있다. 예를 들어, 테스트 데이터를 초기화하거나, 특정 상태를 설정하는 등의 작업을 수행할 수 있다.
AfterEachListener 각 테스트가 실행된 후에 호출된다.
각 테스트 실행 후에 공통적으로 필요한 정리 작업을 수행할 수 있다. 예를 들어, 테스트 이후에 사용한 리소스를 해제하거나, 상태를 초기화하는 등의 작업을 수행할 수 있다
BeforeTestListener 각 테스트 함수가 실행되기 전에 호출된다.
각 테스트 함수 실행 전에 특정한 설정 작업을 수행할 수 있다. 예를 들어, 특정한 테스트 데이터를 초기화하거나, 테스트 환경을 설정하는 등의 작업을 수행할 수 있다.
AfterTestListener 각 테스트 함수가 실행된 후에 호출된다.
각 테스트 함수 실행 후에 정리 작업을 수행할 수 있다. 예를 들어, 테스트 이후에 사용한 리소스를 해제하거나, 상태를 초기화하는 등의 작업을 수행할 수 있다.
BeforeInvocationListener 각 파라미터화된 테스트가 실행되기 전에 호출된다.
AfterInvocationListener 각 파라미터화된 테스트가 실행된 후에 호출된다.
BeforeSpecListener 테스트 스펙이 실행되기 전에 호출된다.
AfterSpecListener 테스트 스펙이 실행된 후에 호출된다.
PrepareSpecListener 테스트 스펙이 실행되기 전에 호출되며, 주로 특정한 작업을 수행하기 위한 준비를 담당한다.
FinalizeSpecListener 테스트 스펙이 실행된 후에 호출되며, 주로 특정한 작업을 수행한 후 자원을 해제하는 등의 마무리 작업을 담당한다.
BeforeProjectListener 프로젝트 내의 모든 테스트가 실행되기 전에 호출된다.
AfterProjectListener 프로젝트 내의 모든 테스트가 실행된 후에 호출된다.

이러한 리스너들은 사용자가 테스트의 실행 전후에 필요한 작업을 수행할 수 있도록 도와주며, 각각의 역할에 따라 특정한 시점에 호출된다. 이를 통해 테스트의 유연성과 재사용성을 높일 수 있다.

고급 확장

고급 확장(Advanced Extensions)은 Kotest에서 제공하는 테스트 실행 과정을 세밀하게 제어하고 사용자 지정 로직을 적용할 수 있는 강력한 기능이다.

다음은 각 고급 확장의 역할과 기능에 대한 설명이다:

확장 설명
ConstructorExtension 테스트 클래스의 생성자를 변경하고 생성자 주입을 사용하여 테스트를 확장한다.
TestCaseExtension 각 테스트 케이스의 실행을 커스터마이징하고 수정한다.
SpecExtension 테스트 스펙의 동작을 변경하고 확장한다.
SpecRefExtension 특정 테스트 스펙 참조에 대한 확장을 수행한다.
DisplayNameFormatterExtension 테스트의 표시 이름을 형식화하고 수정한다.
EnabledExtension 테스트를 활성화 또는 비활성화한다.
ProjectExtension 프로젝트 수준의 확장을 수행하고 전역적인 설정을 제어한다.
SpecExecutionOrderExtension 테스트 스펙의 실행 순서를 조정하고 제어한다.
TagExtension 테스트에 태그를 추가하고 관리한다.
InstantiationErrorListener 테스트 클래스의 인스턴스화 중 발생한 오류를 처리한다.
InstantiationListener 테스트 클래스의 인스턴스화 이벤트를 처리한다.
PostInstantiationExtension 인스턴스화 후에 추가 작업을 수행한다.
IgnoredSpecListener 무시된 테스트 스펙을 처리한다.
SpecFilter 특정 조건에 따라 테스트 스펙을 필터링한다.
TestFilter 특정 조건에 따라 테스트를 필터링한다.

이러한 고급 확장을 사용하여 테스트 실행을 세밀하게 제어하고 원하는 방식으로 사용자 지정할 수 있다.

확장 활용

System Out Listener

확장 기능의 실제 예제로 Kotest에서 제공되는 NoSystemOutListener가 있다. 이 확장 기능은 출력이 표준 출력에 기록되면 오류를 발생시킨다.

package com.devkuma.kotest.tutorial.extensions.ex3

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.extensions.system.NoSystemOutListener

class MyTestSpec : DescribeSpec({

    listener(NoSystemOutListener)

    describe("모든 테스트는 이러한 표준 출력에 기록되서는 안된다.") {
        it("표준 아웃 출력") {
            println("boom") // 실패
        }
    }
})

다음은 예제를 실행한 결과이다:

io.kotest.extensions.system.SystemOutWriteException
	at io.kotest.extensions.system.NoSystemOutListener$setup$1.error(NoSystemOutExtensions.kt:18)
	at io.kotest.extensions.system.NoSystemOutListener$setup$1.print(NoSystemOutExtensions.kt:23)
	at io.kotest.extensions.system.NoSystemOutListener$setup$1.print(NoSystemOutExtensions.kt:17)
	at java.base/java.io.PrintStream.println(PrintStream.java:1054)

... 이하 생략 ...

테스트 코드에 println 함수가 포함되어서 표준 출력이 되어 에러가 발생하였다.

Timer Listener

다른 예제는 각 테스트 케이스에 걸린 시간을 기록을 한다.

다음과 같이 beforeTestafterTest 함수를 사용하여 이를 수행할 수 있다:

package com.devkuma.kotest.tutorial.extensions.ex4

import io.kotest.core.listeners.AfterTestListener
import io.kotest.core.listeners.BeforeTestListener
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult

object TimerListener : BeforeTestListener, AfterTestListener {

    private var started = 0L

    override suspend fun beforeTest(testCase: TestCase) {
        started = System.currentTimeMillis()
    }

    override suspend fun afterTest(testCase: TestCase, result: TestResult) {
        println("Duration of ${testCase.descriptor.parent.id} = " + (System.currentTimeMillis() - started))
    }
}

테스트 코드에는 아래아 같이 등록할 수 있다:

package com.devkuma.kotest.tutorial.extensions.ex4

import io.kotest.core.spec.style.FunSpec

class TimeTest : FunSpec({
    extensions(TimerListener)

    // tests here
    test("TimeTest") {
        println("TimeTest")
    }
})

다음은 예제를 실행한 결과이다:

TimeTest
Duration of DescriptorId(value=com.devkuma.kotest.tutorial.extensions.exam4.TimeTest) = 8

또는, 프로젝트 전체에 등록할 수도 있다.

object MyConfig : AbstractProjectConfig() {
    override fun extensions(): List<Extension> = listOf(TimerListener)
}

참고




최종 수정 : 2024-04-24