Kotest基本拡張(Extensions)
拡張機能
拡張(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)
}
参考
- Introduction to Extensions | Kotest
- Simple Extensions | Kotest
- Advanced Extensions | Kotest
- Extension Examples | Kotest