Kotest 기본 확장(Extensions)
확장 기능
확장(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
CAUTION
일부 확장은 프로젝트 수준에서만 등록할 수 있다. 예를 들어, 스펙 내에BeforeProjectListener
를 등록하면 해당 확장이 발견될 때 이미 프로젝트가 시작되었으므로 아무런 효과가 없다!
간단한 확장
간단한 확장(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
다른 예제는 각 테스트 케이스에 걸린 시간을 기록을 한다.
다음과 같이 beforeTest
및 afterTest
함수를 사용하여 이를 수행할 수 있다:
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)
}
참고
- Introduction to Extensions | Kotest
- Simple Extensions | Kotest
- Advanced Extensions | Kotest
- Extension Examples | Kotest