611 lines
27 KiB
Markdown
611 lines
27 KiB
Markdown
---
|
||
title: kotlin.test и JUnit 5
|
||
description: Полный набор утверждений kotlin.test, аннотации JUnit 5, параметризация, вложенные тесты и жизненный цикл
|
||
tags:
|
||
- kotlin
|
||
- testing
|
||
- junit
|
||
---
|
||
|
||
`kotlin.test` - многоплатформенный API утверждений и аннотаций. На JVM он делегирует работу JUnit 5, поэтому вы получаете стандартный движок и привычную инфраструктуру, но пишете код, который компилируется и для JS, и для Native.
|
||
|
||
```kotlin
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
class StringUtilsTest {
|
||
@Test
|
||
fun `slugify заменяет пробелы`() {
|
||
assertEquals("привет-мир", "Привет Мир".lowercase().replace(" ", "-"))
|
||
}
|
||
}
|
||
```
|
||
|
||
## Подключение
|
||
```kotlin
|
||
// build.gradle.kts
|
||
dependencies {
|
||
testImplementation(kotlin("test"))
|
||
}
|
||
|
||
tasks.test {
|
||
useJUnitPlatform()
|
||
}
|
||
```
|
||
`kotlin("test")` на JVM автоматически выбирает вариант под настроенный движок. Если нужны аннотации JUnit 5 напрямую (`@ParameterizedTest`, `@Nested`), добавляется `org.junit.jupiter:junit-jupiter`.
|
||
|
||
## Все утверждения
|
||
```kotlin
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.assertNotEquals
|
||
import kotlin.test.assertTrue
|
||
import kotlin.test.assertFalse
|
||
import kotlin.test.assertNull
|
||
import kotlin.test.assertNotNull
|
||
import kotlin.test.assertSame
|
||
import kotlin.test.assertNotSame
|
||
import kotlin.test.assertContentEquals
|
||
import kotlin.test.assertContains
|
||
import kotlin.test.assertIs
|
||
import kotlin.test.assertIsNot
|
||
import kotlin.test.assertFailsWith
|
||
|
||
class AllAssertions {
|
||
@Test
|
||
fun `весь набор kotlin test`() {
|
||
assertEquals(5, 2 + 3)
|
||
assertNotEquals(5, 4)
|
||
assertTrue(1 < 2)
|
||
assertFalse(1 > 2)
|
||
assertNull(null)
|
||
assertNotNull("есть")
|
||
assertSame("a", "a") // одна и та же ссылка
|
||
assertNotSame(listOf(1), listOf(1)) // разные объекты
|
||
assertContentEquals(listOf(1, 2), listOf(1, 2)) // поэлементно
|
||
assertContains(listOf(1, 2), 2)
|
||
assertContains("строка", "рок")
|
||
assertIs<String>("текст")
|
||
assertIsNot<Int>("текст")
|
||
assertEquals(0.1 + 0.2, 0.3, absoluteTolerance = 1e-9) // для Double
|
||
|
||
val e = assertFailsWith<IllegalStateException> { error("сбой") }
|
||
assertEquals("сбой", e.message)
|
||
}
|
||
}
|
||
```
|
||
| Утверждение | Проверяет |
|
||
|---|---|
|
||
| `assertEquals(expected, actual)` | равенство через `equals` |
|
||
| `assertNotEquals` | неравенство |
|
||
| `assertTrue` / `assertFalse` | булево условие |
|
||
| `assertNull` / `assertNotNull` | `null` |
|
||
| `assertSame` / `assertNotSame` | идентичность ссылок (`===`) |
|
||
| `assertContentEquals` | поэлементное равенство коллекций и массивов |
|
||
| `assertContains` | вхождение элемента, подстроки, диапазона |
|
||
| `assertIs<T>` / `assertIsNot<T>` | тип, со smart cast после проверки |
|
||
| `assertFailsWith<T> { }` | исключение; возвращает его |
|
||
| `assertEquals(a, b, absoluteTolerance)` | `Double`/`Float` с допуском |
|
||
| `fail("сообщение")` | безусловный провал |
|
||
|
||
Порядок аргументов - `expected, actual`. При провале сообщение гласит «Expected <...>, actual <...>», и перестановка делает отчёт неверным.
|
||
|
||
### assertIs даёт умное приведение
|
||
```kotlin
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertIs
|
||
import kotlin.test.assertEquals
|
||
|
||
sealed interface Result
|
||
data class Ok(val value: String) : Result
|
||
data class Err(val code: Int) : Result
|
||
|
||
fun compute(flag: Boolean): Result = if (flag) Ok("готово") else Err(500)
|
||
|
||
class AssertIsTest {
|
||
@Test
|
||
fun `assertIs приводит тип`() {
|
||
val result = compute(true)
|
||
assertIs<Ok>(result)
|
||
// после assertIs компилятор знает, что result это Ok
|
||
assertEquals("готово", result.value)
|
||
}
|
||
}
|
||
```
|
||
Это преимущество перед `assertTrue(result is Ok)`: там приведения не будет, и понадобится `as`. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md)
|
||
|
||
### assertContentEquals против assertEquals
|
||
```kotlin
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertContentEquals
|
||
import kotlin.test.assertFalse
|
||
import kotlin.test.assertTrue
|
||
|
||
class ContentEqualsTest {
|
||
@Test
|
||
fun `массивы сравниваются только через assertContentEquals`() {
|
||
val a = intArrayOf(1, 2, 3)
|
||
val b = intArrayOf(1, 2, 3)
|
||
|
||
// equals у массивов — это сравнение ссылок
|
||
assertFalse(a == b)
|
||
assertTrue(a.contentEquals(b))
|
||
assertContentEquals(a.toList(), b.toList())
|
||
}
|
||
|
||
@Test
|
||
fun `список и множество не равны даже при одном содержимом`() {
|
||
assertFalse(listOf(1, 2) == setOf(1, 2))
|
||
assertContentEquals(listOf(1, 2), setOf(1, 2).toList())
|
||
}
|
||
}
|
||
```
|
||
Для массивов `assertEquals` сравнивает ссылки и всегда провалится. Подробнее: [Массивы](../Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md)
|
||
|
||
### Исключения
|
||
```kotlin
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.assertFailsWith
|
||
import kotlin.test.assertTrue
|
||
|
||
class Validator {
|
||
fun parse(input: String): Int {
|
||
require(input.isNotBlank()) { "вход пуст" }
|
||
return input.toIntOrNull() ?: throw NumberFormatException("не число: $input")
|
||
}
|
||
}
|
||
|
||
class ExceptionTest {
|
||
private val v = Validator()
|
||
|
||
@Test
|
||
fun `пустой вход даёт IllegalArgumentException`() {
|
||
val e = assertFailsWith<IllegalArgumentException> { v.parse(" ") }
|
||
assertEquals("вход пуст", e.message)
|
||
}
|
||
|
||
@Test
|
||
fun `нечисловой вход даёт NumberFormatException`() {
|
||
val e = assertFailsWith<NumberFormatException> { v.parse("abc") }
|
||
assertTrue(e.message!!.contains("abc"))
|
||
}
|
||
|
||
@Test
|
||
fun `корректный вход не бросает`() {
|
||
assertEquals(42, v.parse("42"))
|
||
}
|
||
}
|
||
```
|
||
`assertFailsWith<T>` требует **точного** типа или подтипа. `assertFailsWith<Exception>` поймает что угодно - это обычно нежелательно, потому что тест перестанет различать ожидаемую ошибку и случайный `NullPointerException`.
|
||
|
||
## Аннотации
|
||
| `kotlin.test` | JUnit 5 | Когда вызывается |
|
||
|---|---|---|
|
||
| `@Test` | `@Test` | каждый тест |
|
||
| `@BeforeTest` | `@BeforeEach` | перед каждым тестом |
|
||
| `@AfterTest` | `@AfterEach` | после каждого теста |
|
||
| - | `@BeforeAll` | один раз перед классом |
|
||
| - | `@AfterAll` | один раз после класса |
|
||
| `@Ignore` | `@Disabled` | пропустить |
|
||
| - | `@DisplayName` | имя в отчёте |
|
||
| - | `@Nested` | вложенный класс |
|
||
| - | `@RepeatedTest` | повторить N раз |
|
||
| - | `@Timeout` | ограничить время |
|
||
| - | `@Tag` | группировка для фильтрации |
|
||
|
||
```kotlin
|
||
import kotlin.test.Test
|
||
import kotlin.test.BeforeTest
|
||
import kotlin.test.AfterTest
|
||
import kotlin.test.Ignore
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.fail
|
||
|
||
class LifecycleTest {
|
||
private lateinit var state: MutableList<String>
|
||
|
||
@BeforeTest
|
||
fun setUp() {
|
||
state = mutableListOf("начальный")
|
||
}
|
||
|
||
@AfterTest
|
||
fun tearDown() {
|
||
state.clear()
|
||
}
|
||
|
||
@Test
|
||
fun `первый тест`() {
|
||
state += "добавлено"
|
||
assertEquals(2, state.size)
|
||
}
|
||
|
||
@Test
|
||
fun `второй тест видит свежее состояние`() {
|
||
assertEquals(1, state.size)
|
||
}
|
||
|
||
@Test
|
||
@Ignore
|
||
fun `пока не реализовано`() {
|
||
fail("не должен выполняться")
|
||
}
|
||
}
|
||
```
|
||
`@Ignore` без причины - плохая практика: тест тихо не выполняется месяцами. В JUnit 5 причину можно указать: `@Disabled("ждёт исправления KT-12345")`.
|
||
|
||
### @BeforeAll требует companion
|
||
```kotlin
|
||
import org.junit.jupiter.api.BeforeAll
|
||
import org.junit.jupiter.api.AfterAll
|
||
import org.junit.jupiter.api.TestInstance
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertTrue
|
||
|
||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||
class BeforeAllTest {
|
||
private var expensiveResource: String = ""
|
||
|
||
@BeforeAll
|
||
fun initOnce() {
|
||
expensiveResource = "дорогой ресурс"
|
||
}
|
||
|
||
@AfterAll
|
||
fun cleanUpOnce() {
|
||
expensiveResource = ""
|
||
}
|
||
|
||
@Test
|
||
fun `ресурс доступен`() {
|
||
assertTrue(expensiveResource.isNotEmpty())
|
||
}
|
||
}
|
||
```
|
||
Без `@TestInstance(PER_CLASS)` метод `@BeforeAll` должен быть статическим, то есть лежать в `companion object` с `@JvmStatic`:
|
||
```kotlin
|
||
import org.junit.jupiter.api.BeforeAll
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertTrue
|
||
|
||
class CompanionBeforeAllTest {
|
||
companion object {
|
||
private var resource: String = ""
|
||
|
||
@JvmStatic
|
||
@BeforeAll
|
||
fun initOnce() {
|
||
resource = "инициализировано"
|
||
}
|
||
}
|
||
|
||
@Test
|
||
fun `ресурс доступен`() {
|
||
assertTrue(resource.isNotEmpty())
|
||
}
|
||
}
|
||
```
|
||
`@TestInstance(PER_CLASS)` короче, но меняет семантику: экземпляр класса создаётся один для всех тестов, и состояние в полях протекает между ними. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||
|
||
## Параметризованные тесты
|
||
```kotlin
|
||
import org.junit.jupiter.params.ParameterizedTest
|
||
import org.junit.jupiter.params.provider.ValueSource
|
||
import org.junit.jupiter.params.provider.CsvSource
|
||
import org.junit.jupiter.params.provider.EnumSource
|
||
import org.junit.jupiter.params.provider.NullAndEmptySource
|
||
import org.junit.jupiter.params.provider.MethodSource
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.assertTrue
|
||
|
||
enum class Status { ACTIVE, BLOCKED, DELETED }
|
||
|
||
class ParameterizedTests {
|
||
@ParameterizedTest
|
||
@ValueSource(strings = ["", " ", "\t", "\n"])
|
||
fun `все эти строки пустые`(input: String) {
|
||
assertTrue(input.isBlank())
|
||
}
|
||
|
||
@ParameterizedTest
|
||
@ValueSource(ints = [1, 2, 3, 5, 8])
|
||
fun `все эти числа положительны`(n: Int) {
|
||
assertTrue(n > 0)
|
||
}
|
||
|
||
@ParameterizedTest
|
||
@CsvSource("2, 3, 5", "0, 0, 0", "-1, 1, 0", "100, -50, 50")
|
||
fun `сложение по таблице`(a: Int, b: Int, expected: Int) {
|
||
assertEquals(expected, a + b)
|
||
}
|
||
|
||
@ParameterizedTest
|
||
@EnumSource(Status::class)
|
||
fun `все статусы имеют непустое имя`(status: Status) {
|
||
assertTrue(status.name.isNotEmpty())
|
||
}
|
||
|
||
@ParameterizedTest
|
||
@NullAndEmptySource
|
||
@ValueSource(strings = [" "])
|
||
fun `null пустая и пробельная строка одинаково бесполезны`(input: String?) {
|
||
assertTrue(input.isNullOrBlank())
|
||
}
|
||
|
||
@ParameterizedTest
|
||
@MethodSource("слоги")
|
||
fun `слоги короткие`(syllable: String) {
|
||
assertTrue(syllable.length <= 3)
|
||
}
|
||
|
||
companion object {
|
||
@JvmStatic
|
||
fun слоги() = listOf("ма", "па", "ба")
|
||
}
|
||
}
|
||
```
|
||
| Источник | Что даёт |
|
||
|---|---|
|
||
| `@ValueSource` | массив литералов одного типа |
|
||
| `@CsvSource` | строки CSV → несколько параметров |
|
||
| `@CsvFileSource` | CSV из ресурса |
|
||
| `@EnumSource` | все значения enum |
|
||
| `@NullSource` / `@EmptySource` / `@NullAndEmptySource` | граничные значения |
|
||
| `@MethodSource` | статический метод, возвращающий коллекцию |
|
||
| `@ArgumentsSource` | свой провайдер |
|
||
|
||
`@MethodSource` на Kotlin требует `@JvmStatic` в `companion object` - иначе JUnit не найдёт метод. Это частая причина ошибки «Could not find factory method».
|
||
|
||
## Вложенные тесты
|
||
```kotlin
|
||
import org.junit.jupiter.api.Nested
|
||
import org.junit.jupiter.api.DisplayName
|
||
import kotlin.test.Test
|
||
import kotlin.test.BeforeTest
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.assertNull
|
||
import kotlin.test.assertTrue
|
||
|
||
class Cache {
|
||
private val store = mutableMapOf<String, String>()
|
||
fun put(k: String, v: String) { store[k] = v }
|
||
fun get(k: String) = store[k]
|
||
fun size() = store.size
|
||
}
|
||
|
||
@DisplayName("Кэш")
|
||
class CacheTest {
|
||
|
||
@Nested
|
||
@DisplayName("когда пуст")
|
||
inner class WhenEmpty {
|
||
private val cache = Cache()
|
||
|
||
@Test
|
||
fun `размер ноль`() {
|
||
assertEquals(0, cache.size())
|
||
}
|
||
|
||
@Test
|
||
fun `любой ключ даёт null`() {
|
||
assertNull(cache.get("любой"))
|
||
}
|
||
}
|
||
|
||
@Nested
|
||
@DisplayName("когда заполнен")
|
||
inner class WhenFilled {
|
||
private val cache = Cache()
|
||
|
||
@BeforeTest
|
||
fun fill() {
|
||
cache.put("ключ", "значение")
|
||
}
|
||
|
||
@Test
|
||
fun `размер один`() {
|
||
assertEquals(1, cache.size())
|
||
}
|
||
|
||
@Test
|
||
fun `известный ключ возвращает значение`() {
|
||
assertEquals("значение", cache.get("ключ"))
|
||
}
|
||
|
||
@Test
|
||
fun `неизвестный ключ даёт null`() {
|
||
assertTrue(cache.get("другой") == null)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
`@Nested` требует `inner class` - вложенный класс в Kotlin по умолчанию статический, и JUnit его не примет. Это даёт группировку в отчёте: «Кэш → когда пуст → размер ноль». Подробнее: [Вложенные и внутренние классы](../ООП/Вложенные%20и%20внутренние%20классы.md)
|
||
|
||
## Повторы и таймауты
|
||
```kotlin
|
||
import org.junit.jupiter.api.RepeatedTest
|
||
import org.junit.jupiter.api.Timeout
|
||
import org.junit.jupiter.api.Tag
|
||
import java.util.concurrent.TimeUnit
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertTrue
|
||
|
||
class RepeatAndTimeoutTest {
|
||
@RepeatedTest(5)
|
||
fun `генератор всегда в диапазоне`() {
|
||
val value = (1..100).random()
|
||
assertTrue(value in 1..100)
|
||
}
|
||
|
||
@Test
|
||
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
|
||
fun `быстрая операция укладывается в срок`() {
|
||
assertTrue((1..1000).sum() > 0)
|
||
}
|
||
|
||
@Test
|
||
@Tag("slow")
|
||
fun `помеченный тест можно отфильтровать`() {
|
||
assertTrue(true)
|
||
}
|
||
}
|
||
```
|
||
Фильтрация по тегам:
|
||
```kotlin
|
||
// build.gradle.kts
|
||
tasks.test {
|
||
useJUnitPlatform {
|
||
excludeTags("slow")
|
||
}
|
||
}
|
||
|
||
tasks.register<Test>("slowTest") {
|
||
useJUnitPlatform {
|
||
includeTags("slow")
|
||
}
|
||
}
|
||
```
|
||
|
||
## Утверждение нескольких условий сразу
|
||
```kotlin
|
||
import org.junit.jupiter.api.assertAll
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.assertTrue
|
||
|
||
data class User(val id: Int, val name: String, val age: Int)
|
||
|
||
class AssertAllTest {
|
||
@Test
|
||
fun `все поля проверяются даже при провале одного`() {
|
||
val user = User(1, "Иван", 30)
|
||
assertAll(
|
||
{ assertEquals(1, user.id) },
|
||
{ assertEquals("Иван", user.name) },
|
||
{ assertTrue(user.age >= 18) },
|
||
)
|
||
}
|
||
}
|
||
```
|
||
Без `assertAll` первый провалившийся `assertEquals` прерывает тест, и об остальных полях вы не узнаете. Это особенно полезно при проверке DTO.
|
||
|
||
## Что не стоит тестировать
|
||
```kotlin
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
data class Point(val x: Int, val y: Int) {
|
||
init {
|
||
require(x >= 0 && y >= 0) { "координаты неотрицательны" }
|
||
}
|
||
|
||
fun distanceTo(other: Point): Double {
|
||
val dx = (x - other.x).toDouble()
|
||
val dy = (y - other.y).toDouble()
|
||
return kotlin.math.sqrt(dx * dx + dy * dy)
|
||
}
|
||
}
|
||
|
||
class PointTest {
|
||
// equals, hashCode, toString, copy генерирует компилятор — не тестируем
|
||
|
||
@Test
|
||
fun `валидация в init работает`() {
|
||
val e = runCatching { Point(-1, 0) }.exceptionOrNull()
|
||
assertEquals("координаты неотрицательны", e?.message)
|
||
}
|
||
|
||
@Test
|
||
fun `расстояние считается верно`() {
|
||
assertEquals(5.0, Point(0, 0).distanceTo(Point(3, 4)), absoluteTolerance = 1e-9)
|
||
}
|
||
}
|
||
```
|
||
Сгенерированные члены `data class` не содержат вашей логики. Тестируйте валидацию в `init`, кастомные методы и всё, что вы написали руками. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||
|
||
## Запуск
|
||
```
|
||
./gradlew test
|
||
./gradlew test --tests "com.example.CacheTest"
|
||
./gradlew test --tests "*Cache*"
|
||
./gradlew test --tests "com.example.CacheTest.WhenEmpty"
|
||
./gradlew test --info
|
||
./gradlew test --rerun-tasks
|
||
```
|
||
Отчёт - `build/reports/tests/test/index.html`.
|
||
|
||
> [!info] Отличия от Java
|
||
> | Java (JUnit 5) | Kotlin (kotlin.test) |
|
||
> |---|---|
|
||
> | `Assertions.assertEquals` | `assertEquals` (top-level) |
|
||
> | `assertThrows(T.class) { }` | `assertFailsWith<T> { }` |
|
||
> | `assertInstanceOf(T.class, x)` | `assertIs<T>(x)` + smart cast |
|
||
> | `assertArrayEquals` | `assertContentEquals` |
|
||
> | `@BeforeEach` | `@BeforeTest` |
|
||
> | `@Disabled` | `@Ignore` |
|
||
> | `void shouldDoX()` | `fun \`делает X\`()` |
|
||
> | `@BeforeAll static` | `companion object` + `@JvmStatic` |
|
||
> | `@Nested static class` | `@Nested inner class` |
|
||
>
|
||
> **`kotlin.test` - фасад, а не фреймворк.** `Assertions` в JUnit 5 - это сам JUnit. `kotlin.test` - тонкий слой, который на JVM делегирует JUnit, на JS - Jasmine/Mocha, на Native - собственный движок. Поэтому код утверждений переносится между платформами без изменений, а инфраструктура остаётся родной для каждой. Цена - набор утверждений беднее, чем у специализированных библиотек: нет цепочек вроде AssertJ.
|
||
>
|
||
> **`assertIs<T>` даёт то, чего `assertInstanceOf` в Java дать не может.** Java-версия возвращает приведённое значение, которое нужно присвоить переменной. `assertIs<T>` объявлен с контрактом `returns() implies (value is T)`, поэтому компилятор применяет умное приведение к исходной переменной. Это следствие механизма контрактов Kotlin. Подробнее: [Проверки и контракты](../Исключения/Проверки%20и%20контракты.md)
|
||
>
|
||
> **Статические методы требуют `companion object` + `@JvmStatic`.** JUnit 5 проектировался под Java, где `static` - модификатор. В Kotlin статики нет, и `@BeforeAll`, `@MethodSource`, `@AfterAll` требуют либо `@JvmStatic` в companion, либо `@TestInstance(PER_CLASS)`. Это самая частая точка трения при работе с JUnit из Kotlin. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||
>
|
||
> **`@Nested` требует `inner`.** В Java вложенный класс нестатический по умолчанию, и `@Nested` работает сразу. В Kotlin всё наоборот: `class Inner` внутри класса статический, и нужен явный `inner`. Без него JUnit сообщает, что `@Nested`-класс должен быть нестатическим.
|
||
>
|
||
> **Проверяемых исключений нет, поэтому `assertFailsWith` важнее.** В Java компилятор заставляет объявить `throws`, и наличие исключения видно в сигнатуре. В Kotlin исключение - невидимая часть контракта, и тест становится единственной документацией того, что функция может упасть. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
|
||
>
|
||
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||
|
||
> [!warning] Подводные камни
|
||
> **Порядок аргументов `assertEquals(expected, actual)`.** Перестановка не вызывает ошибки, но отчёт о провале становится обратным: «Expected <фактическое>, actual <ожидаемое>». Отлаживать такой тест заметно труднее.
|
||
>
|
||
> **`assertEquals` на массивах всегда падает:**
|
||
> ```kotlin
|
||
> import kotlin.test.Test
|
||
> import kotlin.test.assertFalse
|
||
> import kotlin.test.assertTrue
|
||
>
|
||
> class ArrayPitfall {
|
||
> @Test
|
||
> fun `equals у массивов сравнивает ссылки`() {
|
||
> val a = intArrayOf(1, 2)
|
||
> val b = intArrayOf(1, 2)
|
||
> assertFalse(a == b)
|
||
> assertTrue(a.contentEquals(b))
|
||
> }
|
||
> }
|
||
> ```
|
||
> Нужен `assertContentEquals` или `contentEquals`.
|
||
>
|
||
> **`assertEquals` для `Double` без допуска ненадёжен.** `assertEquals(0.3, 0.1 + 0.2)` провалится: результат `0.30000000000000004`. Всегда указывайте `absoluteTolerance`. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md)
|
||
>
|
||
> **`assertEquals(1, 1L)` не компилируется.** Типы `Int` и `Long` различаются, и перегрузки для смешанного сравнения нет. Приводите явно: `assertEquals(1L, value)`.
|
||
>
|
||
> **`@MethodSource` без `@JvmStatic` не находится.** Ошибка - «Could not find factory method». Метод должен лежать в `companion object` и быть помечен `@JvmStatic`, либо класс должен иметь `@TestInstance(PER_CLASS)`.
|
||
>
|
||
> **`@BeforeAll` без `@JvmStatic` молча не вызывается** или даёт ошибку о нестатическом методе. Та же причина.
|
||
>
|
||
> **`@TestInstance(PER_CLASS)` протекает состоянием.** Один экземпляр класса на все тесты означает, что мутация поля в первом тесте видна во втором, и порядок начинает иметь значение. Используйте только когда действительно нужна дорогая инициализация.
|
||
>
|
||
> **`@Nested` без `inner` не работает.** Сообщение JUnit - «@Nested class must not be static». В Kotlin вложенный класс статический по умолчанию.
|
||
>
|
||
> **`assertFailsWith<Exception>` слишком широк.** Он поймает и `NullPointerException` от опечатки в тесте, и тест останется зелёным при неправильной реализации. Указывайте конкретный тип.
|
||
>
|
||
> **Без `useJUnitPlatform()` ничего не запустится.** Gradle по умолчанию использует JUnit 4; тесты JUnit 5 не найдутся, и вывод будет «0 tests found» без ошибки.
|
||
>
|
||
> **Три разных `@Test` в classpath.** `kotlin.test.Test`, `org.junit.jupiter.api.Test` и `org.junit.Test` (JUnit 4). Импорт не того варианта приводит к тому, что тест не запускается. При наличии `kotlin-test` используйте `kotlin.test.Test`.
|
||
>
|
||
> **Обратные кавычки запрещают часть символов.** `` `тест: случай` `` или `` `a.b` `` не скомпилируются - двоеточие, точка, слэши и угловые скобки недопустимы в именах методов JVM.
|
||
>
|
||
> **`@Ignore` без причины превращается в мёртвый код.** Через месяц никто не помнит, почему тест отключён. Используйте `@Disabled("причина")` из JUnit 5.
|
||
>
|
||
> **`assertSame` на строках даёт неожиданный результат.** `assertSame("a", "a")` проходит из-за интернирования литералов, но `assertSame("a", buildString { append("a") })` провалится. Для содержимого используйте `assertEquals`.
|
||
>
|
||
> **Тест в `@BeforeTest` с `lateinit` падает непонятно.** Если `@BeforeTest` бросил исключение, `lateinit`-поле останется неинициализированным, и тест упадёт с `UninitializedPropertyAccessException` - маскируя настоящую причину. Проверяйте вывод `@BeforeTest` в отчёте.
|
||
|
||
Далее: [Kotest](Kotest.md)
|