Kotest Basic Extensions

Kotest Extensions are extension features that provide various capabilities which can be integrated into the test lifecycle of the Kotest test execution framework.

Extensions

Extensions are reusable lifecycle hooks. In fact, lifecycle hooks themselves are represented internally as instances of extensions. In the past, the term listener was used for simple interfaces and extension for advanced interfaces, but they are no longer distinguished and the two terms may be used interchangeably.

With extensions, you can perform additional actions during test execution or integrate custom functionality. Extensions let you control and extend tests more flexibly.

In general, extensions can define work that runs before or after tests, disable tests under specific conditions, or perform logging in a specific way. Extensions are provided as listeners, interceptors, interfaces, and other forms, so they can handle a variety of scenarios.

Basic Extension Usage

The basic usage is to create an implementation of the extension interface you need and register it on a test, spec, or the entire project through ProjectConfig.

For example, the following example creates a before/after spec listener and registers it on a spec:

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")
    }
}

To apply this extension, specify the listener with the extension function as follows:

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

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

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

The following is the result of running the example:

beforeSpec
testSpec
afterSpec

All extensions registered inside a spec are used for every test in that Spec, including test factories and nested tests.

To apply an extension to all specs in the entire project, you can register it with the @AutoScan annotation.

The following example creates a before/after listener and applies it to all specs by adding @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")
    }
}

Unlike the previous example, the following test code does not explicitly specify an extension:

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")
    }
})

The following is the result of running the example:

beforeProject
test1
test2
afterProject

Simple Extensions

Simple Extensions in Kotest provide various listeners so users can perform desired work before and after test execution.

The table below lists the most basic extensions that handle test and spec lifecycle events. Most of them are the same as lifecycle hooks. For advanced extensions that can modify how the engine executes, see the advanced extensions section.

The role and usage of each listener are as follows:

Extension Description
BeforeContainerListener Called before a container, or test group, is executed.
It is mainly used to set up an entire container. For example, you can initialize a specific database or configure external resources.
AfterContainerListener Called after a container, or test group, is executed.
It is mainly used for cleanup work needed after a container. For example, you can close database connections or release external resources.
BeforeEachListener Called before each test runs.
You can perform common setup needed before each test, such as initializing test data or setting a specific state.
AfterEachListener Called after each test runs.
You can perform common cleanup needed after each test, such as releasing resources used by the test or resetting state.
BeforeTestListener Called before each test function runs.
You can perform specific setup before each test function, such as initializing test data or configuring the test environment.
AfterTestListener Called after each test function runs.
You can perform cleanup after each test function, such as releasing resources used by the test or resetting state.
BeforeInvocationListener Called before each parameterized test runs.
AfterInvocationListener Called after each parameterized test runs.
BeforeSpecListener Called before a test spec runs.
AfterSpecListener Called after a test spec runs.
PrepareSpecListener Called before a test spec runs, mainly to prepare for specific work.
FinalizeSpecListener Called after a test spec runs, mainly to finish work such as releasing resources.
BeforeProjectListener Called before all tests in the project run.
AfterProjectListener Called after all tests in the project run.

These listeners help users perform the work they need before and after test execution, and each one is called at a specific point according to its role. This improves test flexibility and reusability.

Advanced Extensions

Advanced Extensions are powerful Kotest features that allow fine-grained control over the test execution process and custom logic.

The following describes the role and behavior of each advanced extension:

Extension Description
ConstructorExtension Changes the constructor of a test class and extends tests with constructor injection.
TestCaseExtension Customizes and modifies the execution of each test case.
SpecExtension Changes and extends the behavior of a test spec.
SpecRefExtension Performs an extension for a specific test spec reference.
DisplayNameFormatterExtension Formats and modifies the display name of a test.
EnabledExtension Enables or disables tests.
ProjectExtension Performs project-level extensions and controls global settings.
SpecExecutionOrderExtension Adjusts and controls the execution order of test specs.
TagExtension Adds and manages tags on tests.
InstantiationErrorListener Handles errors that occur during test class instantiation.
InstantiationListener Handles test class instantiation events.
PostInstantiationExtension Performs additional work after instantiation.
IgnoredSpecListener Handles ignored test specs.
SpecFilter Filters test specs according to specific conditions.
TestFilter Filters tests according to specific conditions.

You can use these advanced extensions to control test execution in detail and customize it as needed.

Using Extensions

System Out Listener

One practical example of an extension is Kotest’s NoSystemOutListener. This extension raises an error when output is written to standard output.

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("No test should write to standard output.") {
        it("standard out output") {
            println("boom") // fails
        }
    }
})

The following is the result of running the example:

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)

... omitted ...

The test code includes a println function, so standard output is produced and an error occurs.

Timer Listener

Another example records the time taken by each test case.

You can do this with the beforeTest and afterTest functions:

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))
    }
}

You can register it in test code as follows:

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")
    }
})

The following is the result of running the example:

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

Or you can register it for the entire project.

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

References