Kotest Lifecycle Hooks
Managing the Test Lifecycle
Managing the test lifecycle means performing the necessary work before and after each test case runs. Kotest provides beforeTest and afterTest blocks so you can manage the test lifecycle. With these blocks, you can perform setup or cleanup work before and after each test case.
Lifecycle Hooks
Sometimes you need to perform work before or after each test case runs. For example, you may need setup or cleanup work such as opening or closing a database connection, or measuring test execution time. Lifecycle hooks are used for this purpose.
Kotest provides many kinds of hooks that can be defined directly inside a spec. For more advanced use cases, such as distributable plugins or reusable hooks, you can use extensions.
At the end of this section, you can find the available hooks and when they are executed.
There are several ways to use hooks in Kotest.
Writing Hooks with DSL Methods
The first and simplest method is to use DSL methods available inside a spec to create and register a TestListener. For example, you can call beforeTest and afterTest directly as follows.
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!")
}
}
})
The following is the result of running the example:
Starting a test TestCase(descriptor=TestDescriptor... omitted ...
Starting a test TestCase(descriptor=TestDescriptor... omitted ...
Johnny5 is alive!
Finished spec with result Success(duration=6ms)
Finished spec with result Success(duration=51ms)
In the background, these DSL methods create an instance of TestListener that overrides the appropriate functions and registers it so the listener is executed.
You can use afterProject as a DSL method to create a project listener, but there is no beforeProject because the project has already started by the time the framework is detecting specs.
DSL Methods that Accept Functions
These DSL methods accept functions, so you can extract the logic into a function and reuse it in multiple places.
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")
}
}
})
The following is the result of running the example:
Starting a test TestCase(descriptor=TestDescriptor... omitted ...
boom
Starting a test TestCase(descriptor=TestDescriptor... omitted ...
devkuma is alive!
Overriding Callback Functions in a Spec
The second method is to override callback functions in the spec. The example below is a variation of the first method.
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!")
}
}
}
}
Lifecycle Callbacks
To understand all callbacks correctly, it is important to understand the possible TestType values:
Container: a container that can include other testsTest: a leaf test that cannot include nested tests. In other words, a test that can run independently without invoking other testsDynamic: can be either a container or a test, and is used when tests are dynamically added by features such as property tests or data tests
Kotest provides several callback functions that run according to the test lifecycle.
The following table describes each callback used in Kotest:
| Callback | Description |
|---|---|
beforeContainer |
Called before all tests in a test container are executed. |
afterContainer |
Called after all tests in a test container are executed. |
beforeEach |
Called before each test case runs. Use this when you want to perform separate setup for each test. |
afterEach |
Called after each test case runs. Use this when you want to perform separate cleanup for each test. |
beforeAny |
Called before all tests in a specific container or spec run. Use this for common setup for all tests in a container or spec. |
afterAny |
Called after all tests in a specific container or spec run. Use this for common cleanup for all tests in a container or spec. |
beforeTest |
Called before each test method runs. You can use this function to set up each test. It behaves the same as beforeAny. |
afterTest |
Called after each test method runs. You can use this function to clean up each test. It behaves the same as afterAny. |
beforeSpec |
Called before each spec runs. Use this for setup that applies to the whole spec. For example, you can open a database connection or initialize a specific resource. When IsolationMode is SingleInstance, it behaves the same as prepareSpec. |
afterSpec |
Called after each spec runs. Use this for cleanup that applies to the whole spec. For example, you can close a database connection or clean up a specific resource. When IsolationMode is SingleInstance, it behaves the same as finalizeSpec. If an exception occurs in this callback, subsequent beforeSpec and AfterSpec callbacks are skipped. |
prepareSpec |
Called before a spec runs, and used to perform custom initialization work. When IsolationMode is SingleInstance, it behaves the same as beforeSpec. |
finalizeSpec |
Called after a spec runs, and used to perform custom cleanup work. When IsolationMode is SingleInstance, it behaves the same as afterSpec. |
beforeInvocation |
Called before each test method or test case runs. If there is no invocation configuration, it behaves the same as beforeTest. |
afterInvocation |
Called after each test method or test case runs. If there is no invocation configuration, it behaves the same as afterTest. |
These callbacks let you perform specific actions at various points, such as before and after each test or before and after each spec. Each callback is invoked at its corresponding point, and you can implement the logic you need.
beforeAny and beforeTest are simply different names for the same function, but they differ from beforeEach. beforeAny and beforeTest are called before TestType.Test, while beforeEach is called before both TestType.Container and TestType.Test. The same applies to afterAny, afterTest, and afterEach.
The following example shows when each callback function is executed:
package com.devkuma.kotest.tutorial.lifecycle.ex4
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CallbackTest : StringSpec({
// Callback called before each spec runs
beforeSpec {
println("beforeSpec: perform setup before the spec runs.")
// Add setup work for the whole spec.
}
// Callback called after each spec runs
afterSpec {
println("afterSpec: perform cleanup after the spec runs.")
// Add cleanup work for the whole spec.
}
// Callback called before each test runs
beforeTest { testCase ->
println("beforeTest: perform setup before ${testCase.name.testName} runs.")
// Add common setup work for tests here.
}
// Callback called after each test runs
afterTest { (testCase, testResult) ->
println("afterTest: perform cleanup after ${testCase.name.testName} runs.")
println("Result: $testResult")
// Add common cleanup work for tests here.
}
// Callback called before all tests run
beforeAny { testCase ->
println("beforeAny: perform setup before ${testCase.name.testName} test runs.")
// Add common setup work for all tests here.
}
// Callback called after all tests run
afterAny { (testCase, testResult) ->
println("afterAny: perform cleanup after ${testCase.name.testName} test runs.")
println("Result: $testResult")
// Add common cleanup work for all tests here.
}
// Callback called before each test runs
beforeEach { testCase ->
println("beforeEach: perform setup before ${testCase.name.testName} test runs.")
// Add individual setup work for tests here.
}
// Callback called after each test runs
afterEach { (testCase, testResult) ->
println("afterEach: perform cleanup after ${testCase.name.testName} test runs.")
println("Result: $testResult")
// Add individual cleanup work for tests here.
}
// Test case 1
"TestCase1" {
println("Run TestCase1.")
val result = 2 + 2
result shouldBe 4
}
// Test case 2
"TestCase2" {
println("Run 'TestCase2'.")
val list = mutableListOf<Int>()
list.add(1)
list.add(2)
list.size shouldBe 2
}
})
The following is the result of running the example:
beforeSpec: perform setup before the spec runs.
beforeEach: perform setup before TestCase1 test runs.
beforeTest: perform setup before TestCase1 runs.
beforeAny: perform setup before TestCase1 test runs.
Run TestCase1.
afterAny: perform cleanup after TestCase1 test runs. Result: Success(duration=36ms)
afterTest: perform cleanup after TestCase1 runs. Result: Success(duration=36ms)
afterEach: perform cleanup after TestCase1 test runs. Result: Success(duration=36ms)
beforeEach: perform setup before TestCase2 test runs.
beforeTest: perform setup before TestCase2 runs.
beforeAny: perform setup before TestCase2 test runs.
Run 'TestCase2'.
afterAny: perform cleanup after TestCase2 test runs. Result: Success(duration=1ms)
afterTest: perform cleanup after TestCase2 runs. Result: Success(duration=1ms)
afterEach: perform cleanup after TestCase2 test runs. Result: Success(duration=1ms)
afterSpec: perform cleanup after the spec runs.