--- title: Kotest description: Спецификации, матчеры, data-driven тесты, property-based тестирование, жизненный цикл и изоляция tags: - kotlin - testing - kotest --- Kotest - полноценный фреймворк тестирования, написанный на Kotlin и для Kotlin. Он даёт три независимо применимых вещи: DSL спецификаций вместо `@Test`, богатый набор матчеров и property-based тестирование. ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain class StringSpecDemo : StringSpec({ "длина строки" { "абвг".length shouldBe 4 } "матчеры строк" { "привет мир" shouldContain "мир" } }) ``` Тест - это строка-описание и лямбда. Никаких аннотаций, никаких ограничений на имена. ## Подключение ```kotlin // build.gradle.kts dependencies { testImplementation("io.kotest:kotest-runner-junit5:6.0.4") testImplementation("io.kotest:kotest-assertions-core:6.0.4") testImplementation("io.kotest:kotest-property:6.0.4") } kotlin { jvmToolchain(21) } tasks.test { useJUnitPlatform() } ``` | Артефакт | Зачем | |---|---| | `kotest-runner-junit5` | движок + спецификации | | `kotest-assertions-core` | матчеры `shouldBe` и остальные | | `kotest-property` | property-based тестирование | | `kotest-framework-datatest` | `withData` (входит в runner) | | `kotest-assertions-json` | матчеры для JSON | | `kotest-extensions-testcontainers` | интеграция с Testcontainers | Три части используются независимо: матчеры работают в обычных `@Test`-классах JUnit, а спецификации - без матчеров Kotest. ## Стили спецификаций Kotest предлагает семь стилей. Все эквивалентны по возможностям - выбор только о читаемости. ### StringSpec - самый лаконичный ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.collections.shouldHaveSize class StringSpecStyle : StringSpec({ "пустой список имеет размер ноль" { emptyList() shouldHaveSize 0 } "сложение работает" { (2 + 3) shouldBe 5 } }) ``` ### FunSpec - с явным `test` и группами ```kotlin import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.collections.shouldHaveSize class FunSpecStyle : FunSpec({ test("сложение") { (2 + 3) shouldBe 5 } context("операции со списком") { test("размер") { listOf(1, 2) shouldHaveSize 2 } test("первый элемент") { listOf(1, 2).first() shouldBe 1 } } }) ``` ### DescribeSpec - в стиле Jest и RSpec ```kotlin import io.kotest.core.spec.style.DescribeSpec import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe class DescribeSpecStyle : DescribeSpec({ describe("калькулятор") { it("складывает") { (1 + 1) shouldBe 2 } context("при делении на ноль") { it("бросает ArithmeticException") { shouldThrow { 1 / 0 } } } } }) ``` ### BehaviorSpec - given/when/then ```kotlin import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.collections.shouldHaveSize class BehaviorSpecStyle : BehaviorSpec({ given("пустой список") { `when`("добавляем элемент") { then("размер становится один") { mutableListOf().apply { add(1) } shouldHaveSize 1 } } } }) ``` `when` - ключевое слово Kotlin, поэтому обратные кавычки обязательны. ### Остальные три ```kotlin import io.kotest.core.spec.style.ShouldSpec import io.kotest.core.spec.style.WordSpec import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.collections.shouldHaveSize class ShouldSpecStyle : ShouldSpec({ should("работать без обёрток") { true shouldBe true } }) class WordSpecStyle : WordSpec({ "список" When { "пуст" should { "иметь размер 0" { emptyList() shouldHaveSize 0 } } } }) class FreeSpecStyle : FreeSpec({ "группа" - { "вложенный тест" { 1 shouldBe 1 } } }) ``` | Стиль | Синтаксис теста | Когда удобен | |---|---|---| | `StringSpec` | `"имя" { }` | простые плоские тесты | | `FunSpec` | `test("имя") { }`, `context { }` | универсальный выбор | | `DescribeSpec` | `describe`, `context`, `it` | привычен после Jest | | `BehaviorSpec` | `given`, `when`, `then` | BDD-сценарии | | `ShouldSpec` | `should("имя") { }` | плоские тесты с акцентом на поведение | | `WordSpec` | `"x" When { "y" should { } }` | многословные описания | | `FreeSpec` | `"группа" - { "тест" { } }` | произвольная вложенность | В отчёте вложенность отображается деревом: ``` └─ Kotest ✔ ├─ DescribeSpecStyle ✔ │ └─ калькулятор ✔ │ ├─ складывает ✔ │ └─ при делении на ноль ✔ │ └─ бросает ArithmeticException ✔ └─ BehaviorSpecStyle ✔ └─ Given: пустой список ✔ └─ When: добавляем элемент ✔ └─ Then: размер становится один ✔ ``` ## Матчеры ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldStartWith import io.kotest.matchers.string.shouldEndWith import io.kotest.matchers.string.shouldMatch import io.kotest.matchers.string.shouldBeBlank import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.collections.shouldBeSorted import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.ints.shouldBeGreaterThan import io.kotest.matchers.ints.shouldBeInRange import io.kotest.matchers.doubles.shouldBeWithinPercentageOf import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.types.shouldBeInstanceOf import io.kotest.matchers.maps.shouldContainKey import io.kotest.matchers.result.shouldBeSuccess class MatchersDemo : StringSpec({ "равенство" { 5 shouldBe 5 5 shouldNotBe 4 } "строки" { "привет мир" shouldContain "мир" "привет мир" shouldStartWith "прив" "привет мир" shouldEndWith "мир" "abc123" shouldMatch Regex("[a-z]+\\d+") " ".shouldBeBlank() } "коллекции" { listOf(1, 2, 3) shouldContainExactly listOf(1, 2, 3) listOf(1, 2, 3) shouldContainAll listOf(3, 1) listOf(1, 2) shouldHaveSize 2 listOf(1, 2, 3).shouldBeSorted() emptyList().shouldBeEmpty() } "числа" { 5 shouldBeGreaterThan 3 5 shouldBeInRange 1..10 (0.1 + 0.2).shouldBeWithinPercentageOf(0.3, 0.001) } "null и типы" { null.shouldBeNull() "есть".shouldNotBeNull() "текст".shouldBeInstanceOf() } "map и Result" { mapOf("k" to 1) shouldContainKey "k" runCatching { 42 }.shouldBeSuccess() } }) ``` | Группа | Примеры | |---|---| | общие | `shouldBe`, `shouldNotBe`, `shouldBeSameInstanceAs` | | строки | `shouldContain`, `shouldMatch`, `shouldBeBlank`, `shouldHaveLength` | | коллекции | `shouldContainExactly`, `shouldContainAll`, `shouldBeSorted`, `shouldBeUnique` | | числа | `shouldBeGreaterThan`, `shouldBeInRange`, `shouldBeWithinPercentageOf` | | null | `shouldBeNull`, `shouldNotBeNull` | | типы | `shouldBeInstanceOf`, `shouldBeTypeOf` | | map | `shouldContainKey`, `shouldContainValue` | | `Result` | `shouldBeSuccess`, `shouldBeFailure` | | файлы | `shouldExist`, `shouldBeADirectory` | Матчеры инфиксные, поэтому читаются слева направо: `actual shouldBe expected` - порядок обратный `assertEquals`, и это намеренно. ### shouldNotBeNull даёт умное приведение ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe class SmartCastDemo : StringSpec({ "после shouldNotBeNull тип известен" { val value: String? = "текст" value.shouldNotBeNull() // компилятор знает, что value это String value.length shouldBe 5 } }) ``` ## Исключения ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.throwables.shouldThrowAny import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain class ExceptionsDemo : StringSpec({ "shouldThrow возвращает исключение" { val e = shouldThrow { require(false) { "проверка не прошла" } } e.message shouldBe "проверка не прошла" } "shouldThrowExactly требует точный тип" { shouldThrowExactly { error("сбой") } } "shouldThrowAny ловит любое" { val e = shouldThrowAny { 1 / 0 } e.message.shouldContain("zero") } "shouldNotThrowAny для успешного пути" { shouldNotThrowAny { listOf(1).first() } } }) ``` `shouldThrow` принимает подтипы, `shouldThrowExactly` - только точный тип. Разница важна для иерархий исключений. Подробнее: [Система исключений](../Исключения/Система%20исключений.md) ## assertSoftly и inspectors ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.assertions.assertSoftly import io.kotest.inspectors.forAll import io.kotest.inspectors.forExactly import io.kotest.inspectors.forAtLeastOne import io.kotest.matchers.shouldBe import io.kotest.matchers.ints.shouldBeGreaterThan import io.kotest.matchers.string.shouldStartWith data class User(val id: Int, val name: String, val age: Int) class SoftAndInspectors : StringSpec({ "assertSoftly проверяет все условия" { val user = User(1, "Иван", 30) assertSoftly { user.id shouldBe 1 user.name shouldBe "Иван" user.age shouldBeGreaterThan 18 } } "assertSoftly на объекте как receiver" { val user = User(2, "Пётр", 25) assertSoftly(user) { id shouldBe 2 name shouldStartWith "П" } } "inspectors проверяют элементы коллекции" { val users = listOf(User(1, "Иван", 30), User(2, "Пётр", 25)) users.forAll { it.age shouldBeGreaterThan 18 } users.forExactly(1) { it.name shouldBe "Иван" } users.forAtLeastOne { it.age shouldBeGreaterThan 28 } } }) ``` `assertSoftly` собирает все провалы в одно сообщение вместо остановки на первом. Inspectors - это проверки с кванторами: `forAll`, `forNone`, `forExactly(n)`, `forAtLeastOne`, `forAny`, `forSome`. ## Data-driven тесты ```kotlin import io.kotest.core.spec.style.FunSpec import io.kotest.datatest.withData import io.kotest.matchers.shouldBe data class AddCase(val a: Int, val b: Int, val sum: Int) class DataDriven : FunSpec({ withData( AddCase(2, 3, 5), AddCase(0, 0, 0), AddCase(-1, 1, 0), AddCase(100, -50, 50), ) { (a, b, sum) -> (a + b) shouldBe sum } withData( nameFn = { "строка длины ${it.length} пуста" }, "", " ", " ", ) { s -> s.isBlank() shouldBe true } withData( mapOf( "положительное" to 5, "ноль" to 0, "отрицательное" to -5, ), ) { n -> (n * 0) shouldBe 0 } }) ``` Каждый случай становится отдельным тестом в отчёте: ``` ├─ DataDriven ✔ │ ├─ AddCase(a=2, b=3, sum=5) ✔ │ ├─ AddCase(a=0, b=0, sum=0) ✔ │ ├─ AddCase(a=-1, b=1, sum=0) ✔ │ └─ AddCase(a=100, b=-50, sum=50) ✔ ``` Имя теста берётся из `toString()` аргумента, поэтому `data class` для случаев удобнее кортежей. Форма с `Map` даёт явные имена - предпочтительна, когда `toString()` неинформативен. ## Property-based тестирование Вместо конкретных примеров задаётся свойство, которое должно выполняться для любых входных данных. Kotest генерирует случайные значения и проверяет свойство на каждом. ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.property.Arb import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.string import io.kotest.property.arbitrary.list import io.kotest.property.arbitrary.filter import io.kotest.property.arbitrary.map import io.kotest.property.checkAll import io.kotest.property.forAll import io.kotest.matchers.shouldBe import io.kotest.matchers.collections.shouldHaveSize class PropertyBased : StringSpec({ "сложение коммутативно" { checkAll { a, b -> (a + b) shouldBe (b + a) } } "reverse дважды даёт исходную строку" { checkAll(Arb.string()) { s -> s.reversed().reversed() shouldBe s } } "forAll с булевым результатом" { forAll(Arb.int(1..100), Arb.int(1..100)) { a, b -> a + b > a } } "sorted не меняет размер" { checkAll(Arb.list(Arb.int(), 0..20)) { l -> l.sorted() shouldHaveSize l.size } } "производные генераторы" { checkAll(Arb.int().filter { it != 0 }) { n -> (n / n) shouldBe 1 } checkAll(Arb.int(1..100).map { it * 2 }) { even -> (even % 2) shouldBe 0 } } }) ``` Разница `checkAll` и `forAll`: первый ожидает утверждений внутри лямбды, второй - возврата `Boolean`. | Генератор | Что даёт | |---|---| | `Arb.int()`, `Arb.int(1..100)` | числа, с диапазоном | | `Arb.string()`, `Arb.string(5..10)` | строки | | `Arb.boolean()`, `Arb.double()`, `Arb.long()` | примитивы | | `Arb.list(gen, 0..10)`, `Arb.set(gen)` | коллекции | | `Arb.enum()` | значения enum | | `Arb.of(a, b, c)` | из фиксированного набора | | `Arb.bind(g1, g2) { a, b -> ... }` | составные объекты | | `gen.filter { }`, `gen.map { }` | производные | | `Arb.int().orNull()` | с вероятностью `null` | ### Shrinking Главная ценность property-based тестов - автоматический поиск минимального контрпримера. При провале Kotest уменьшает входные данные: ``` Attempting to shrink arg 960 Shrink #1: 0 pass Shrink #2: 1 pass Shrink #3: 320 pass Shrink #4: 480 pass Shrink #5: 640 fail ... Shrink result (after 124 shrinks) => 500 Property failed after 3 attempts Repeat this test by using seed 2234853214744756250 ``` Вместо случайного `960` сообщение называет точную границу `500`. Seed позволяет воспроизвести тот же набор значений - его стоит сохранять в тесте при отладке: ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.property.Arb import io.kotest.property.PropTestConfig import io.kotest.property.arbitrary.int import io.kotest.property.checkAll import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual class SeedDemo : StringSpec({ "воспроизводимый прогон" { checkAll( PropTestConfig(seed = 2234853214744756250, iterations = 50), Arb.int(0..1000), ) { n -> n shouldBeGreaterThanOrEqual 0 } } }) ``` По умолчанию - 1000 итераций; `PropTestConfig(iterations = n)` меняет их число. ## Жизненный цикл ```kotlin import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe class LifecycleDemo : FunSpec({ var counter = 0 beforeSpec { counter = 100 } beforeTest { counter += 1 } afterTest { counter -= 1 } afterSpec { counter = 0 } test("первый") { counter shouldBe 101 } test("второй") { counter shouldBe 101 } }) ``` | Коллбэк | Когда | |---|---| | `beforeSpec` | один раз перед всеми тестами класса | | `beforeTest` | перед каждым тестом | | `beforeContainer` | перед группой (`context`, `describe`) | | `afterTest` | после каждого теста | | `afterSpec` | один раз после всех тестов | | `beforeEach` / `afterEach` | только листовые тесты | | `afterProject` | после всего прогона (в ProjectConfig) | Коллбэки - лямбды внутри конструктора, а не методы с аннотациями, поэтому `@JvmStatic` и `companion object` не нужны - в отличие от JUnit 5. ## Изоляция - главная ловушка По умолчанию Kotest создаёт **один** экземпляр спецификации на все тесты, и состояние протекает: ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe class DefaultIsolation : StringSpec({ var state = 0 "первый" { state += 1 state shouldBe 1 } "второй видит изменения первого" { state += 1 state shouldBe 2 // не 1! } }) ``` Это противоположно JUnit, где экземпляр создаётся на каждый тест. Исправляется явно: ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.core.spec.IsolationMode import io.kotest.matchers.shouldBe class IsolatedSpec : StringSpec({ isolationMode = IsolationMode.InstancePerLeaf var state = 0 "первый видит нулевое состояние" { state += 1 state shouldBe 1 } "второй тоже видит нулевое состояние" { state += 1 state shouldBe 1 } }) ``` | Режим | Поведение | |---|---| | `SingleInstance` (по умолчанию) | один экземпляр на спецификацию | | `InstancePerLeaf` | новый экземпляр на каждый листовой тест | | `InstancePerRoot` | новый экземпляр на каждый корневой тест | `InstancePerLeaf` даёт поведение, привычное после JUnit, но замедляет прогон: конструктор спецификации выполняется заново на каждый тест. ## Конфигурация тестов ```kotlin import io.kotest.core.spec.style.FunSpec import io.kotest.core.annotation.Ignored import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import kotlin.time.Duration.Companion.seconds class ConfigDemo : FunSpec({ test("повторяется трижды").config(invocations = 3, timeout = 2.seconds) { (1 + 1) shouldBe 2 } test("отключён").config(enabled = false) { error("не выполняется") } test("по условию").config(enabledIf = { System.getenv("CI") == null }) { true shouldBe true } test("помечен тегом").config(tags = setOf(Slow)) { true shouldBe true } }) object Slow : io.kotest.core.Tag() @Ignored class IgnoredSpec : StringSpec({ "никогда не запускается" { error("нет") } }) ``` | Параметр `.config()` | Что делает | |---|---| | `invocations` | повторить N раз | | `timeout` | ограничение времени | | `enabled` | включить/выключить | | `enabledIf` | условие по лямбде | | `tags` | теги для фильтрации | | `retries`, `retryDelay` | повторы при провале | | `severity` | уровень важности для фильтрации | | `coroutineDebugProbes` | трассировка корутин при зависании | ## Конфигурация проекта ```kotlin import io.kotest.core.config.AbstractProjectConfig import io.kotest.core.spec.IsolationMode import io.kotest.core.names.DuplicateTestNameMode import io.kotest.engine.concurrency.SpecExecutionMode class ProjectConfig : AbstractProjectConfig() { override val isolationMode = IsolationMode.InstancePerLeaf override val specExecutionMode = SpecExecutionMode.LimitedConcurrency(4) override val duplicateTestNameMode = DuplicateTestNameMode.Error override val globalAssertSoftly = true } ``` Файл с классом, наследующим `AbstractProjectConfig`, Kotest находит автоматически. Это единственное место, где можно задать режим изоляции для всего проекта - иначе придётся повторять в каждой спецификации. | Свойство | Что задаёт | |---|---| | `isolationMode` | изоляция для всех спецификаций | | `specExecutionMode` | `Sequential`, `Concurrent`, `LimitedConcurrency(n)` для спецификаций | | `testExecutionMode` | то же для тестов внутри спецификации | | `duplicateTestNameMode` | `Warn` (по умолчанию), `Error`, `Silent` | | `globalAssertSoftly` | оборачивать каждый тест в `assertSoftly` | | `timeout`, `invocationTimeout`, `projectTimeout` | таймауты | ## Фильтрация и запуск ``` ./gradlew test ./gradlew test --tests "com.example.CacheSpec" ./gradlew test -Dkotest.tags="!Slow" ./gradlew test -Dkotest.tags="Integration & !Slow" ``` ```kotlin // build.gradle.kts tasks.test { useJUnitPlatform() systemProperty("kotest.tags", "!Slow") } ``` Движок Kotest работает поверх JUnit Platform, поэтому Gradle, IDE и CI видят тесты как обычные - отчёты, фильтры и интеграции работают без настройки. ## Kotest и JUnit вместе Матчеры Kotest не требуют его движка: ```kotlin import kotlin.test.Test import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import io.kotest.matchers.collections.shouldHaveSize class MixedTest { @Test fun `матчеры Kotest в обычном тесте JUnit`() { "привет мир" shouldContain "мир" listOf(1, 2) shouldHaveSize 2 (2 + 3) shouldBe 5 } } ``` Это разумный компромисс: читаемые матчеры без перехода на другой фреймворк. Достаточно `kotest-assertions-core`, `kotest-runner-junit5` не нужен. ## Когда брать Kotest **Стоит**, если нужны: вложенные спецификации с группировкой, data-driven тесты (`withData` короче `@ParameterizedTest`), property-based тестирование, богатые матчеры, тесты для Multiplatform. **Не стоит**, если: проект небольшой и `kotlin.test` хватает, команда привыкла к JUnit, важна минимальная зависимость. Подробнее: [kotlin.test и JUnit 5](kotlin.test%20и%20JUnit%205.md) Промежуточный вариант - `kotest-assertions-core` поверх JUnit: самая заметная часть выгоды при минимальных изменениях. > [!info] Отличия от Java > | Java | Kotest | > |---|---| > | JUnit 5 + AssertJ | один фреймворк | > | `@Test void x()` | `"описание" { }` | > | `assertThat(x).isEqualTo(y)` | `x shouldBe y` | > | `@ParameterizedTest` + `@CsvSource` | `withData(...)` | > | jqwik для property-based | `kotest-property` встроен | > | `@Nested static class` | вложенность через DSL | > | `@BeforeAll static` | `beforeSpec { }` | > | новый экземпляр на тест | один экземпляр по умолчанию | > > **DSL спецификаций возможен из-за лямбд с receiver.** Spock даёт похожий синтаксис в Groovy, но ценой динамической типизации и отдельного языка. Kotest строит DSL на типизированных лямбдах с получателем: `StringSpec({ ... })` - это конструктор, принимающий лямбду, внутри которой `String.invoke(block)` - оператор. Весь синтаксис - обычный Kotlin с проверкой типов. Подробнее: [DSL](../Функциональный%20Kotlin/DSL.md) > > **Матчеры читаются обратно `assertEquals`.** `x shouldBe y` ставит фактическое значение первым, `assertEquals(expected, actual)` - ожидаемое. Это следствие инфиксной формы: по-английски читается как предложение. При переходе с JUnit порядок приходится переучивать, зато перепутать его труднее - неправильный порядок звучит неестественно. > > **Property-based тестирование встроено, а не отдельная библиотека.** В Java это jqwik или QuickTheories - сторонние инструменты со своим стилем тестов. В Kotest `checkAll` работает внутри любой спецификации рядом с обычными тестами, и shrinking находит минимальный контрпример автоматически. Это снижает порог: добавить одно свойство к существующим тестам ничего не стоит. > > **Изоляция устроена противоположно JUnit, и это главный источник ошибок.** JUnit создаёт новый экземпляр тестового класса на каждый тест - изоляция по умолчанию. Kotest создаёт один экземпляр на спецификацию, потому что спецификация - это лямбда в конструкторе, и её повторное выполнение стоит дороже. Результат: переменные в теле спецификации разделяются между тестами, и порядок начинает иметь значение. Решается `IsolationMode.InstancePerLeaf`, но включать нужно осознанно. > > Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) > [!warning] Подводные камни > **Изоляции по умолчанию нет.** `var` в теле спецификации разделяется между всеми тестами, и второй тест видит изменения первого. Это противоположно JUnit и ломает тесты неочевидным образом - особенно при параллельном запуске. Задавайте `IsolationMode.InstancePerLeaf` в `ProjectConfig`, если не уверены. > > **Kotest собран под JVM target 11.** Компиляция с дефолтным `-jvm-target 1.8` даёт ошибку «cannot inline bytecode built with JVM target 11 into bytecode that is being built with JVM target 1.8». Нужен `jvmToolchain(11)` или выше - для Gradle это обычно уже так, но при ручной компиляции флаг обязателен. Подробнее: [kotlinc](../Инструментарий/kotlinc.md) > > **`withData` даёт дублирующиеся имена при похожих значениях.** Строки `" "` и `" "` после обработки дают одинаковое имя теста, Kotest выводит «WARN: Duplicated test name» и добавляет префикс `(1)`. Используйте `nameFn = { ... }` или форму с `Map`, где имена заданы явно. > > **`when` требует обратных кавычек в `BehaviorSpec`.** `when("...")` не скомпилируется - это ключевое слово. Нужно `` `when`("...") ``. В `WordSpec` аналогично используется `When` с заглавной буквы. > > **`shouldBe` на массивах сравнивает ссылки.** Как и `assertEquals`: `intArrayOf(1) shouldBe intArrayOf(1)` провалится. Нужен `shouldContainExactly` или преобразование в список. Подробнее: [Массивы](../Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md) > > **`checkAll` по умолчанию делает 1000 итераций.** Для медленной проверки (обращение к БД, сложный алгоритм) это минуты. Задавайте `PropTestConfig(iterations = 100)` явно. > > **Генератор может не находить контрпример.** `Arb.int()` редко выдаёт конкретное значение вроде `Int.MAX_VALUE`, хотя Kotest и подмешивает граничные случаи. Если свойство ломается только на краях, добавьте их явно через `Arb.of(...)` или `Arb.int().withEdgecases(...)`. > > **Провал property-теста без сохранённого seed невоспроизводим.** Сообщение содержит seed - его нужно скопировать в `PropTestConfig(seed = ...)`, иначе следующий прогон сгенерирует другие значения, и ошибка «исчезнет». > > **`shouldThrow` ловит подтипы.** `shouldThrow` пройдёт на любом исключении, включая `NullPointerException` от ошибки в самом тесте. Указывайте конкретный тип или `shouldThrowExactly`. > > **`assertSoftly` не ловит исключения.** Он собирает провалы матчеров, но `NullPointerException` внутри блока прервёт тест сразу, и остальные проверки не выполнятся. > > **Вложенные `context` без тестов внутри молча пропускаются.** Группа без листовых тестов не считается провалом - она просто не появляется в отчёте, и легко не заметить, что проверки не выполняются. > > **Параллелизм спецификаций и тестов настраивается раздельно.** `specExecutionMode` влияет только на спецификации; тесты внутри одной спецификации по умолчанию идут последовательно, пока не задан `testExecutionMode = TestExecutionMode.Concurrent`. Старые `parallelism` и `.config(threads = n)` в Kotest 6 убраны - код из статей по версии 5 не скомпилируется. > > **Две версии Kotest в classpath дают неочевидные ошибки.** Артефакты `kotest-runner-junit5`, `kotest-assertions-core` и `kotest-property` должны быть одной версии; расхождение проявляется как `NoSuchMethodError` в рантайме. Держите версию в `libs.versions.toml`. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md) > > **`ProjectConfig` находится по наследованию, а не по имени.** Класс должен наследовать `AbstractProjectConfig` и лежать в тестовом source set; имя значения не имеет, но два таких класса в проекте дают неопределённое поведение. Далее: [MockK](MockK.md)