Kotest構成(Configuration)

Kotest構成(Configuration)は、テスト実行の動作をカスタマイズし制御するために使われる。さまざまな設定オプションを提供し、ユーザーがテスト環境を望む形に調整できる。

テストケース構成

各テストはさまざまなパラメータで構成できる。テスト名の後に構成関数を呼び出し、設定したいパラメータを渡す。使用可能なパラメータは次のとおりである。

  • invocations: このテストを実行する回数である。非決定的なテストがあり、特定のテストを決まった回数だけ実行して最終的に失敗するかを確認したい場合に便利である。すべての呼び出しが成功した場合のみテストは成功する。デフォルトは1である。
  • threads: スレッド数を設定し、このテストの呼び出しを並列化できる。値は呼び出し数以下でなければならない。同様に、呼び出し数をスレッド数と同じ値に設定すると、各呼び出しに固有のスレッドが割り当てられる。
  • enabled: falseに設定すると、このテストは無効化される。テストを一時的に無視する必要がある場合に便利である。このパラメータをブール式と一緒に使い、特定条件でのみテストを実行することもできる。
  • enabledIf: enabledと同じ機能を提供するが、テストケースの実行時期になったときに遅延評価される関数である。
  • timeout: このテストのタイムアウトを設定する。指定時間内にテストが完了しない場合、テストは失敗する。非決定的で完了しない可能性があるコードに便利である。タイムアウトは2.seconds3.minutesなどのようにインスタンス化できるkotlin.Duration型である。
  • tags: テストをグループ化するために使用できるタグのセットである。詳細は下記を参照する。
  • listeners: このテストでのみ実行するテストリスナーを登録する。
  • extensions: このテストでのみ実行する拡張を登録する。

テストでの構成設定例である。

import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe

class MyTests : ShouldSpec() {
    init {
        should("return the length of the string").config(invocations = 10, threads = 2) {
            "sammy".length shouldBe 5
            "".length shouldBe 0
        }
    }
}
import io.kotest.core.spec.style.WordSpec
import io.kotest.matchers.shouldBe
import kotlin.time.Duration.Companion.seconds

class MyTests : WordSpec() {
    init {
        "String.length" should {
            "return the length of the string".config(timeout = 2.seconds) {
                "sammy".length shouldBe 5
                "".length shouldBe 0
            }
        }
    }
}
import io.kotest.core.spec.style.FunSpec
import io.kotest.core.Tag

class FunSpecTest : FunSpec() {
    init {
        test("FunSpec should support config syntax").config(tags = setOf(Database, Linux)) {
            // ...
        }
    }
}

object Database : Tag()
object Linux : Tag()

Specのすべてのテストケースに対して、デフォルトのTestCaseConfigを指定することもできる。

defaultTestCaseConfig関数を再定義する。

import io.kotest.core.spec.style.StringSpec
import io.kotest.core.test.config.TestCaseConfig

class MySpec : StringSpec() {

    override fun defaultTestCaseConfig() = TestCaseConfig(invocations = 3)

    init {
        // your test cases ...
    }
}

または、defaultTestConfig値への代入でも可能である。

import io.kotest.core.spec.style.FunSpec
import io.kotest.core.test.config.TestCaseConfig

class FunSpecTest : FunSpec() {
    init {

        defaultTestConfig = TestCaseConfig(enabled = true, invocations = 3)

        test("FunSpec should support Spec config syntax in init{} block") {
            // ...
        }
    }
}

プロジェクト構成

Kotestは柔軟であり、Spec内でテスト順序を構成したり、テストクラスの生成方法を構成したりするなど、さまざまな方法でテストを構成できる。ときにはグローバルレベルで設定したいことがあり、その場合はproject-level-configを使う必要がある。

プロジェクトレベル構成は、AbstractProjectConfigを拡張するオブジェクトまたはクラスを作成して使用できる。

Specレベルまたはテストで直接設定したすべての構成は、プロジェクトレベルで指定された構成を上書きする。

KotestProjectConfigで使用できる構成オプションには、テスト並列処理、無視されたテストによるSpec失敗、グローバルAssertSoftly、再利用可能なリスナーまたは拡張などがある。

ランタイム検出

実行時、KotestはAbstractProjectConfigを拡張するクラスを検索し、そのクラスに定義されたすべての構成値を使ってインスタンス化する。

異なるモジュールに複数の設定クラスを作成でき、現在のクラスパスにあるすべてのクラスが検出され、設定がマージされる。これは共通設定をルートモジュールに配置できるため有効である。競合が発生した場合は1つの値が任意に選択されるため、異なる設定クラスに競合する設定を追加することは推奨されない。

大規模プロジェクトで起動コストが高い場合、これらの構成クラスの自動検索を無効化できる。システムプロパティまたは環境変数kotest.framework.classpath.scanning.config.disabletrueに設定する。

自動スキャンを無効化した後もプロジェクト構成を使うには、Kotestがリフレクションでインスタンス化できるよう、よく知られたクラス名を指定する。使用するシステムプロパティまたは環境変数はkotest.framework.config.fqnである。

たとえば、次の設定を行う。

kotest.framework.classpath.scanning.config.disable=true
kotest.framework.config.fqn=com.wibble.KotestConfig

ランタイムスキャンを無効化し、com.wibble.KotestConfigクラスを探す。このクラスは依然としてAbstractProjectConfigを継承している必要がある。

並列処理

並列処理レベル、デフォルトは1、を設定することで、複数コアを持つ現代的なCPUを活用するためにSpecを並列実行するようKotestに要求できる。Spec内のテストは常に順次実行される。

これを行うには、構成内でparallelismを再定義し、1より大きい値に設定する。設定された数値は同時に実行されるSpec数である。例:

object KotestProjectConfig : AbstractProjectConfig() {
    override val parallelism = 3
}

これを有効にする別の方法として、システムプロパティkotest.framework.parallelismがあり、定義されている場合は常にこの値より優先される。

一部のテストは並列実行に適さない場合があるため、個別Specを除外し、Specに@DoNotParallelizeアノテーションを使用して個別に実行させることができる。

Assertionモード

Kotest Assertionを使用しないテストが実行された場合、ビルドを失敗させるか、標準エラーへ警告を出すよう要求できる。

これを行うには、構成内でassertionModeAssertionMode.ErrorまたはAssertionMode.Warnに設定する。これを有効にする別の方法として、システムプロパティkotest.framework.assertion.modeがあり、定義されている場合は常にこの値より優先される。

object KotestProjectConfig : AbstractProjectConfig() {
    override val assertionMode = AssertionMode.Error
}

グローバルAssert Softly

Assert Softlyは、複数のエラーを1つの失敗としてまとめるのに非常に便利である。すべてのテストでこの機能を自動的に有効化するには、構成で設定できる。これを有効にする別の方法として、システムプロパティkotest.framework.assertion.globalassertsoftlytrueに設定する方法があり、定義されている場合は常にこの値より優先される。

object KotestProjectConfig : AbstractProjectConfig() {
    override val globalAssertSoftly = true
}

重複テスト名の処理

デフォルトでは、Kotestは同じスコープ内の他のテストと同じ名前を持つ場合、テスト名を変更する。テスト名に_1_2などを追加する。この機能は自動生成されたテストに便利である。

この動作は、duplicateTestNameModeDuplicateTestNameMode.ErrorまたはDuplicateTestNameMode.Warnに設定してグローバルに変更できる。

Errorは重複名でテストスイートを失敗させ、警告は名前を変更しつつ警告を出力する。

無視されたテストの失敗

無視されたテストを失敗として扱いたい場合がある。この機能を有効化するには、プロジェクト構成でfailOnIgnoredTeststrueに設定する。

例:

object KotestProjectConfig : AbstractProjectConfig() {
    override val failOnIgnoredTests = true
}

テスト順序

KotestはSpecとテストに対して独立して順序を反映できるようサポートする。

テスト順序

1つのSpecで複数のテストを実行するとき、実行方法には特定の順序がある。

デフォルトでは順次順序、つまりSpecに定義されたテスト順序が使用されるが、この順序は変更できる。使用可能なオプションはテスト順序を参照する。

Spec順序

デフォルトではSpecクラスの順序は定義されていない。デフォルト設定が不要な場合はこれで十分だが、Specの実行順序を制御する必要がある場合はSpec順序を使用できる。

テストネーミング

テスト名はさまざまな方法で調整できる。

テストケース

テスト名の大文字小文字はtestNameCaseの値を変更して制御できる。

デフォルトでは値は変更されないTestNameCase.AsIsである。

値をTestNameCase.Lowercaseに設定すると、テスト名が小文字で出力される。

テスト名に接頭辞を追加するSpecを使用する場合、これはWordSpecまたはBehaviorSpecでなければならないが、TestNameCase.SentenceおよびTestNameCase.InitialLowercaseの値が役立つ場合がある。

テスト名タグ

テスト名に使う別のオプションはtestNameAppendTagsであり、trueに設定するとテスト名に該当するタグがすべて含まれる。たとえば、Specにlinuxおよびsparkタグを持つテストfooが定義されている場合、テスト名はfoo [linux, spark]に調整される。

この設定は、システムプロパティまたは環境変数kotest.framework.testname.append.tagstrueにすることでも設定できる。

テスト名の空白

複数行にわたってテスト名を定義する場合、removeTestNameWhitespaceが便利なことがある。次の例を見てみよう。

"""this is
   my test case""" {
  // test here
}

この場合、出力のテスト名はthis is my test caseである。removeTestNameWhitespacetrueに設定すると、この名前はthis is my test caseのように空白が削除される。

この機能を有効にする別の方法として、システムプロパティkotest.framework.testname.multilinetrueに設定する方法があり、定義されている場合は常にこの値より優先される。

object KotestProjectConfig : AbstractProjectConfig() {
    override val testNameRemoveWhitespace = true
}

フレームワーク構成プロパティ

KotestEngineProperties.kt

package io.kotest.core.internal

object KotestEngineProperties {

   const val scriptsEnabled = "kotest.framework.scripts.enabled"

   const val dumpConfig = "kotest.framework.dump.config"

   /**
    * Sets the tag expression that determines included/excluded tags.
    */
   const val tagExpression = "kotest.tags"

   const val excludeTags = "kotest.tags.exclude"

   const val includeTags = "kotest.tags.include"

   /**
    * A regex expression that is used to match the test [io.kotest.core.descriptors.Descriptor]'s path
    * to determine if a test should be included in the test plan or not.
    */
   const val filterTests = "kotest.filter.tests"

   /**
    * A regex expression that is used to match the [io.kotest.mpp.bestName] of a class
    * to determine if a spec should be included in the test plan or not.
    */
   const val filterSpecs = "kotest.filter.specs"

   const val propertiesFilename = "kotest.properties.filename"

   /**
    * If set to true, then source ref's will not be created for test cases.
    * This may speed up builds (as the engine will not need to create stack traces to
    * generate line numbers) but will also reduce functionality in the intellij plugin
    * (by limiting the ability to drill directly into the test inside a file).
    */
   const val disableSourceRef = "kotest.framework.sourceref.disable"

   /**
    * If set to true, disables the use of '!' as a prefix to disable tests.
    */
   const val disableBangPrefix = "kotest.bang.disable"

   /**
    * The default [io.kotest.core.spec.IsolationMode] for specs.
    */
   const val isolationMode = "kotest.framework.isolation.mode"

   /**
    * The default [io.kotest.core.test.AssertionMode] for tests.
    */
   const val assertionMode = "kotest.framework.assertion.mode"

   /**
    * The default parallelism for specs.
    */
   const val parallelism = "kotest.framework.parallelism"

   /**
    * The default timeout for test cases.
    */
   const val timeout = "kotest.framework.timeout"

   /**
    * The default timeout for the entire test suite.
    */
   const val projectTimeout = "kotest.framework.projecttimeout"

   const val logLevel = "kotest.framework.loglevel"

   /**
    * The default timeout for each invocation of a test case.
    */
   const val invocationTimeout = "kotest.framework.invocation.timeout"

   const val disableTestNestedJarScanning = "kotest.framework.disable.test.nested.jar.scanning"

   const val concurrentSpecs = "kotest.framework.spec.concurrent"

   const val concurrentTests = "kotest.framework.test.concurrent"

   /**
    * Disable scanning the classpath for configuration classes by setting this property to true
    */
   const val disableConfigurationClassPathScanning = "kotest.framework.classpath.scanning.config.disable"

   /**
    * Specify a fully qualified name to use for project config.
    * This class will be instantiated via reflection.
    */
   const val configurationClassName = "kotest.framework.config.fqn"

   /**
    * Disable scanning the classpath for listeners with @AutoScan by setting this property to true
    */
   const val disableAutoScanClassPathScanning = "kotest.framework.classpath.scanning.autoscan.disable"

   const val allowMultilineTestName = "kotest.framework.testname.multiline"

   /**
    *  If set -> filter testCases by this severity level and higher, else running all
    */
   const val testSeverity = "kotest.framework.test.severity"

   /**
    * Enable assert softly globally.
    * */
   const val globalAssertSoftly = "kotest.framework.assertion.globalassertsoftly"

   /**
    * Appends all tags associated with a test case to its display name.
    * */
   const val testNameAppendTags = "kotest.framework.testname.append.tags"

   /**
    * Controls whether classes will inherit tags from their supertypes. Default false
    */
   const val tagInheritance = "kotest.framework.tag.inheritance"

   /**
    * Controls the [io.kotest.core.names.DuplicateTestNameMode] mode.
    */
   const val duplicateTestNameMode = "kotest.framework.testname.duplicate.mode"

   const val disableJarDiscovery = "kotest.framework.discovery.jar.scan.disable"
}

参照