Kotest 라이프사이클 후크(Lifecycle Hooks)
테스트 라이프사이클 관리
테스트 라이프사이클 관리는 각 테스트 케이스의 실행 전후에 필요한 작업을 수행하는 것을 의미한다. Kotest는 beforeTest
와 afterTest
블록을 제공하여 테스트 라이프사이클을 관리할 수 있다. 이를 통해 각 테스트 케이스의 실행 전후에 필요한 설정 또는 정리 작업을 수행할 수 있다.
라이프사이클 후크
각 테스트 케이스의 실행 전후에 어떤 작업을 수행해야 하는 경우가 있다. 예를 들어, 데이터베이스 연결을 설정하거나 정리하는 작업, 테스트 시간을 측정 등의 필요한 설정 또는 정리 작업이 있을 수 있다. 이때 사용하는 것이 라이프사이클 후크(Lifecycle Hooks)이다.
Kotest는 스펙 내에서 직접 정의할 수 있는 다양한 종류의 후크를 제공한다. 배포 가능한 플러그인이나 재사용 가능한 후크 작성과 같은 더 발전된 경우의 확장(Extentions)을 사용할 수도 있다.
이 섹션의 마지막에는 사용 가능한 후크 목록과 실행 시기가 나와 있다.
Kotest에서 후크를 사용하는 방법에는 여러 가지가 있다:
DSL 메서드으로 작성하기
첫번째로 가장 간단한 방법은 스펙 내에서 사용 가능한 DSL 메서드를 사용하여 TestListener
를 생성하고 등록하는 것이다. 예를 들어, 다음과 같이 beforeTest
, afterTest
를 직접 호출할 수 있다.
package com.devkuma.kotest.tutorial.lifecycle.ex1
import io.kotest.core.spec.style.WordSpec
class TestSpec : WordSpec({
beforeTest {
println("Starting a test $it")
}
afterTest { (test, result) ->
println("Finished spec with result $result")
}
"this test" should {
"be alive" {
println("devkuma is alive!")
}
}
})
다음은 예제를 실행한 결과이다:
Starting a test TestCase(descriptor=TestDescriptor... 생략 ...
Starting a test TestCase(descriptor=TestDescriptor... 생략 ...
Johnny5 is alive!
Finished spec with result Success(duration=6ms)
Finished spec with result Success(duration=51ms)
백그라운드에서 이러한 DSL 메서드는 적절한 함수를 재정의하고, 이 테스트 리스너가 실행되도록 등록한 TestListener
의 인스턴스를 생성한다.
프로젝트 리스너의 인스턴스를 생성하는 DSL 메서드로 afterProject
를 사용할 수 있지만, 프레임워크가 스펙을 감지하는 이 단계에 있을 때는 이미 프로젝트가 시작되었으므로 beforeProject
는 없다!
함수를 사용하는 DSL 메서드
이러한 DSL 메서드는 함수를 받으므로, 함수에 대한 로직을 가져와 여러 곳에서 재사용할 수 있다.
package com.devkuma.kotest.tutorial.lifecycle.ex2
import io.kotest.core.spec.style.WordSpec
import io.kotest.core.test.TestCase
val startTest: suspend (TestCase) -> Unit = {
println("Starting a test $it")
}
class TestSpec : WordSpec({
// used once
beforeTest(startTest)
"this test" should {
"be alive" {
println("devkuma is alive!")
}
}
})
class OtherSpec : WordSpec({
// used twice
beforeTest(startTest)
"this test" should {
"fail" {
println("boom")
}
}
})
다음은 예제를 실행한 결과이다:
Starting a test TestCase(descriptor=TestDescriptor... 생략 ...
boom
Starting a test TestCase(descriptor=TestDescriptor... 생략 ...
devkuma is alive!
스펙에서 콜백 함수 재정의하기
두 번째 방법으로는 스펙에서 콜백 함수를 재정의하는 것이다. 아래 예제는 첫 번째 방법을 변형하였다.
package com.devkuma.kotest.tutorial.lifecycle.ex3
import io.kotest.core.spec.style.WordSpec
import io.kotest.core.test.TestCase
class TestSpec : WordSpec() {
override suspend fun beforeTest(testCase: TestCase) {
println("Starting a test $testCase")
}
init {
"this test" should {
"be alive" {
println("devkuma is alive!")
}
}
}
}
라이프사이클 콜백
모든 콜백을 올바르게 이해하려면, 가능한 TestType
값을 이해하는 것이 중요하다:
Container
: 다른 테스트를 포함할 수 있는 컨테이너Test
: 중첩된 테스트를 포함할 수 없는 리프(Leaf) 테스트. 즉, 다른 테스트를 호출하지 않고 독립적으로 실행될 수 있는 테스트Dynamic
: 컨테이너 또는 테스트가 될 수 있으며, 속성 테스트 또는 데이터 테스트와 같은 기능을 통해 테스트가 동적으로 추가될 때 사용
Kotest에서는 테스트 수명주기에 따라 실행되는 여러 종류의 콜백 함수를 제공한다.
아래는 Kotest에서 사용되는 각 콜백에 대한 설명이다:
콜백 | 설명 |
---|---|
beforeContainer |
컨테이너(Test container) 내의 모든 테스트가 실행되기 전에 호출되는 콜백이다. |
afterContainer |
컨테이너(Test container) 내의 모든 테스트가 실행된 후에 호출되는 콜백이다. |
beforeEach |
각 테스트 케이스(Test case) 실행 전에 호출되는 콜백이다. 각 테스트마다 별도의 설정을 수행하고 싶을 때 사용된다. |
afterEach |
각 테스트 케이스(Test case) 실행 후에 호출되는 콜백이다. 각 테스트마다 별도의 정리 작업을 수행하고 싶을 때 사용된다. |
beforeAny |
특정 컨테이너 또는 스펙 내의 모든 테스트가 실행되기 전에 호출되는 콜백이다. 특정 컨테이너나 스펙 내의 모든 테스트에 대한 공통적인 설정 작업을 수행하고자 할 때 사용된다. |
afterAny |
특정 컨테이너 또는 스펙 내의 모든 테스트가 실행된 후에 호출되는 콜백이다. 특정 컨테이너나 스펙 내의 모든 테스트에 대한 공통적인 정리 작업을 수행하고자 할 때 사용된다. |
beforeTest |
각 테스트 메소드(Test method) 실행 전에 호출되는 콜백이다. 이 함수를 사용하여 각 테스트의 설정을 수행할 수 있다. beforeAny와 동일한 동작을 한다. |
afterTest |
각 테스트 메소드(Test method) 실행 후에 호출되는 콜백이다. 이 함수를 사용하여 각 테스트의 정리 작업을 수행할 수 있다. afterAny와 동일한 동작을 한다. |
beforeSpec |
각 스펙(Spec) 실행 전에 호출되는 콜백이다. 스펙 전체에 대한 설정 작업을 수행하고자 할 때 사용된다. 예를 들어, 데이터베이스 연결을 설정하거나 특정 자원을 초기화하는 작업을 수행할 수 있다. IsolationMode 가 SingleInstance 인 경우, prepareSpec 과 같은 동작을 한다. |
afterSpec |
각 스펙(Spec) 실행 후에 호출되는 콜백이다. 스펙 전체에 대한 정리 작업을 수행하고자 할 때 사용된다. 예를 들어, 데이터베이스 연결을 닫거나 특정 자원을 정리하는 작업을 수행할 수 있다. IsolationMode 가 SingleInstance 인 경우, finalizeSpec 과 같은 동작을 한다. 해당 Callback에서 예외가 발생하면 이후 beforeSpec , AfterSpec 은 스킵된다. |
prepareSpec |
스펙(Spec)이 실행되기 전에 호출되는 콜백으로, 사용자 정의 초기화 작업을 수행하는 데 사용된다.IsolationMode 가 SingleInstance인 경우, beforeSpec과 같은 동작을 한다. |
finalizeSpec |
스펙(Spec)이 실행된 후에 호출되는 콜백으로, 사용자 정의 정리 작업을 수행하는 데 사용된다.IsolationMode 가 SingleInstanc e인 경우, afterSpec 과 같은 동작을 한다. |
beforeInvocation |
각 테스트 메소드(Test method) 또는 테스트 케이스(Test case) 실행 전에 호출되는 콜백이다. invocation 설정이 없으면 beforeTest와 같은 동작을 한다. |
afterInvocation |
각 테스트 메소드(Test method) 또는 테스트 케이스(Test case) 실행 후에 호출되는 콜백이다. invocation 설정이 없으면 afterTest와 같은 동작을 한다. |
이러한 콜백들은 각 테스트 실행 전후나 스펙 실행 전후 등의 다양한 시점에 특정 동작을 수행할 수 있도록 도와준다. 각 콜백은 해당하는 시점에 호출되며, 필요에 따라 적절한 로직을 구현하여 사용할 수 있다.
beforeAny
과 beforeTest
는 동일한 함수의 다른 이름일 뿐이지만, beforeEach
와는 다르다. beforeAny
, beforeTest
각각 TestType.Test
전에 호출되는 반면, beforeEach
는 TestType.Container
와 TestType.Test
전에 호출된다. afterAny
, afterTest
및 afterEach
에도 동일하게 적용된다.
다음 콜백 함수가 언제 실행되는 알수 있는 예시를 통해서 알아보겠다:
package com.devkuma.kotest.tutorial.lifecycle.ex4
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CallbackTest : StringSpec({
// 각 스펙이 실행되기 전에 호출되는 콜백
beforeSpec {
println("beforeSpec: 스펙 실행 전 설정을 수행.")
// 스펙 전체에 대한 설정 작업을 추가.
}
// 각 스펙이 실행된 후에 호출되는 콜백
afterSpec {
println("afterSpec: 스펙 실행 후 정리를 수행.")
// 스펙 전체에 대한 정리 작업을 추가.
}
// 각 테스트 실행 전에 호출되는 콜백
beforeTest { testCase ->
println("beforeTest: ${testCase.name.testName} 실행 전 설정을 수행.")
// 여기에 테스트의 공통 설정 작업을 추가.
}
// 각 테스트 실행 후에 호출되는 콜백
afterTest { (testCase, testResult) ->
println("afterTest: ${testCase.name.testName} 실행 후 정리를 수행.")
println("결과: $testResult")
// 여기에 테스트의 공통 정리 작업을 추가.
}
// 모든 테스트가 실행되기 전에 호출되는 콜백
beforeAny { testCase ->
println("beforeAny: ${testCase.name.testName} 테스트 실행 전 설정을 수행.")
// 여기에 모든 테스트에 대한 공통 설정 작업을 추가다.
}
// 모든 테스트가 실행된 후에 호출되는 콜백
afterAny { (testCase, testResult) ->
println("afterAny:${testCase.name.testName} 테스트 실행 후 정리를 수행.")
println("결과: $testResult")
// 여기에 모든 테스트에 대한 공통 정리 작업을 추가.
}
// 각 테스트 실행 전에 호출되는 콜백
beforeEach { testCase ->
println("beforeEach: ${testCase.name.testName} 테스트 실행 전 설정을 수행.")
// 여기에 테스트의 개별 설정 작업을 추가.
}
// 각 테스트 실행 후에 호출되는 콜백
afterEach { (testCase, testResult) ->
println("afterEach: ${testCase.name.testName} 테스트 실행 후 정리를 수행.")
println("결과: $testResult")
// 여기에 테스트의 개별 정리 작업을 추가.
}
// 테스트 케이스 1
"TestCase1" {
println("TestCase1 실행.")
val result = 2 + 2
result shouldBe 4
}
// 테스트 케이스 2
"TestCase2" {
println("'TestCase2' 실행.")
val list = mutableListOf<Int>()
list.add(1)
list.add(2)
list.size shouldBe 2
}
})
다음은 위 예시를 실행한 결과이다:
beforeSpec: 스펙 실행 전 설정을 수행.
beforeEach: TestCase1 테스트 실행 전 설정을 수행.
beforeTest: TestCase1 실행 전 설정을 수행.
beforeAny: TestCase1 테스트 실행 전 설정을 수행.
TestCase1 실행.
afterAny:TestCase1 테스트 실행 후 정리를 수행. 결과: Success(duration=36ms)
afterTest: TestCase1 실행 후 정리를 수행. 결과: Success(duration=36ms)
afterEach: TestCase1 테스트 실행 후 정리를 수행. 결과: Success(duration=36ms)
beforeEach: TestCase2 테스트 실행 전 설정을 수행.
beforeTest: TestCase2 실행 전 설정을 수행.
beforeAny: TestCase2 테스트 실행 전 설정을 수행.
'TestCase2' 실행.
afterAny:TestCase2 테스트 실행 후 정리를 수행. 결과: Success(duration=1ms)
afterTest: TestCase2 실행 후 정리를 수행. 결과: Success(duration=1ms)
afterEach: TestCase2 테스트 실행 후 정리를 수행. 결과: Success(duration=1ms)
afterSpec: 스펙 실행 후 정리를 수행.