Kotest基本拡張(Extensions)

KotestのExtensionsは、Kotestテスト実行フレームワークのテストライフサイクルに統合できるさまざまな機能を提供する拡張機能である。

拡張機能

拡張(Extensions)は、再利用可能なライフサイクルフックである。実際、ライフサイクルフック自体も内部的には拡張のインスタンスとして表現される。以前は単純なインターフェースにはリスナーという用語を、高度なインターフェースには拡張という用語を使っていたが、現在は両者を区別せず、2つの用語を混用できる。

拡張を活用すると、テスト実行中に追加の動作を行ったり、ユーザー定義の機能を統合したりできる。拡張を使うことで、テストをより柔軟に制御し拡張できる。

一般にExtensionsは、テスト前後に実行される作業を定義したり、特定の条件でテストを無効化したり、特定の方法でロギングを行ったりできる。Extensionsはリスナー、インターセプター、インターフェースなどの形で提供され、さまざまなシナリオに対応できる。

拡張の基本的な使い方

基本的な使い方は、必要な拡張インターフェースの実装を作成し、それをテスト、Spec、またはプロジェクト全体のProjectConfigに登録することである。

たとえば、以下の例ではSpec前後のリスナーを作成し、それを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")
    }
}

この拡張機能を適用するには、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内に登録されたすべての拡張は、そのSpecのすべてのテスト、つまりテストファクトリやネストされたテストを含むすべてに使用される。

プロジェクト全体のすべてのSpecに拡張機能を適用するには、@AutoScanアノテーションを指定して登録できる。

以下の例では前後リスナーを作成し、@AutoScanを指定することで全Specに適用している。

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が提供するさまざまなリスナーを使って、テスト実行前後にユーザーが必要な作業を行えるようにする。

以下の表では、テストおよびSpecのライフサイクルイベントを扱う最も基本的な拡張を示しており、その多くはライフサイクルフックと同じである。エンジンの実行方式を変更できる高度な拡張については、高度な拡張を参照すること。

各リスナーの役割と使い方は次のとおりである。

拡張 説明
BeforeContainerListener コンテナ、つまりテストグループが実行される前に呼び出される。
主にコンテナ全体を設定する作業に使用される。たとえば、特定のデータベースを初期化したり、外部リソースを設定したりできる。
AfterContainerListener コンテナ、つまりテストグループが実行された後に呼び出される。
主にコンテナ後に必要な後処理に使用される。たとえば、データベース接続を閉じたり、外部リソースを解放したりできる。
BeforeEachListener 各テストが実行される前に呼び出される。
各テスト実行前に共通して必要な設定作業を行える。たとえば、テストデータを初期化したり、特定の状態を設定したりできる。
AfterEachListener 各テストが実行された後に呼び出される。
各テスト実行後に共通して必要な後処理を行える。たとえば、テスト後に使用したリソースを解放したり、状態を初期化したりできる。
BeforeTestListener 各テスト関数が実行される前に呼び出される。
各テスト関数実行前に特定の設定作業を行える。たとえば、特定のテストデータを初期化したり、テスト環境を設定したりできる。
AfterTestListener 各テスト関数が実行された後に呼び出される。
各テスト関数実行後に後処理を行える。たとえば、テスト後に使用したリソースを解放したり、状態を初期化したりできる。
BeforeInvocationListener 各パラメータ化テストが実行される前に呼び出される。
AfterInvocationListener 各パラメータ化テストが実行された後に呼び出される。
BeforeSpecListener テストSpecが実行される前に呼び出される。
AfterSpecListener テストSpecが実行された後に呼び出される。
PrepareSpecListener テストSpecが実行される前に呼び出され、主に特定の作業を行うための準備を担当する。
FinalizeSpecListener テストSpecが実行された後に呼び出され、主に特定の作業後にリソースを解放するなどの仕上げ作業を担当する。
BeforeProjectListener プロジェクト内のすべてのテストが実行される前に呼び出される。
AfterProjectListener プロジェクト内のすべてのテストが実行された後に呼び出される。

これらのリスナーは、ユーザーがテストの実行前後に必要な作業を行えるようにし、それぞれの役割に応じたタイミングで呼び出される。これにより、テストの柔軟性と再利用性を高められる。

高度な拡張

高度な拡張(Advanced Extensions)は、Kotestが提供するテスト実行過程を細かく制御し、ユーザー定義ロジックを適用できる強力な機能である。

各高度な拡張の役割と機能は次のとおりである。

拡張 説明
ConstructorExtension テストクラスのコンストラクタを変更し、コンストラクタインジェクションを使ってテストを拡張する。
TestCaseExtension 各テストケースの実行をカスタマイズし、変更する。
SpecExtension テストSpecの動作を変更し、拡張する。
SpecRefExtension 特定のテストSpec参照に対する拡張を行う。
DisplayNameFormatterExtension テストの表示名をフォーマットし、変更する。
EnabledExtension テストを有効化または無効化する。
ProjectExtension プロジェクトレベルの拡張を行い、グローバル設定を制御する。
SpecExecutionOrderExtension テストSpecの実行順序を調整し、制御する。
TagExtension テストにタグを追加し、管理する。
InstantiationErrorListener テストクラスのインスタンス化中に発生したエラーを処理する。
InstantiationListener テストクラスのインスタンス化イベントを処理する。
PostInstantiationExtension インスタンス化後に追加作業を行う。
IgnoredSpecListener 無視されたテストSpecを処理する。
SpecFilter 特定の条件に従ってテストSpecをフィルタリングする。
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)
}

参考