Files
obsidian/Kotlin/Тестирование/Kotest.md
T

723 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<Int>() 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<ArithmeticException> { 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<Int>().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<Int>() 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<Int>().shouldBeEmpty()
}
"числа" {
5 shouldBeGreaterThan 3
5 shouldBeInRange 1..10
(0.1 + 0.2).shouldBeWithinPercentageOf(0.3, 0.001)
}
"null и типы" {
null.shouldBeNull()
"есть".shouldNotBeNull()
"текст".shouldBeInstanceOf<String>()
}
"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<T>`, `shouldBeTypeOf<T>` |
| 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<IllegalArgumentException> {
require(false) { "проверка не прошла" }
}
e.message shouldBe "проверка не прошла"
}
"shouldThrowExactly требует точный тип" {
shouldThrowExactly<IllegalStateException> { error("сбой") }
}
"shouldThrowAny ловит любое" {
val e = shouldThrowAny { 1 / 0 }
e.message.shouldContain("zero")
}
"shouldNotThrowAny для успешного пути" {
shouldNotThrowAny { listOf(1).first() }
}
})
```
`shouldThrow<T>` принимает подтипы, `shouldThrowExactly<T>` - только точный тип. Разница важна для иерархий исключений. Подробнее: [Система исключений](../Исключения/Система%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<Int, Int> { 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<Status>()` | значения 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<T>` ловит подтипы.** `shouldThrow<Exception>` пройдёт на любом исключении, включая `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)