Claude
Skills
Sign in
Back

kotlin-testing

Included with Lifetime
$97 forever

Patrones de pruebas Kotlin con Kotest, MockK, pruebas de coroutines, pruebas basadas en propiedades y cobertura con Kover. Sigue la metodología TDD con prácticas idiomáticas de Kotlin.

General

What this skill does


# Patrones de Pruebas Kotlin

Patrones completos de pruebas Kotlin para escribir pruebas confiables y mantenibles siguiendo la metodología TDD con Kotest y MockK.

## Cuándo Usar

- Escribir nuevas funciones o clases Kotlin
- Agregar cobertura de pruebas a código Kotlin existente
- Implementar pruebas basadas en propiedades
- Seguir el flujo de trabajo TDD en proyectos Kotlin
- Configurar Kover para cobertura de código

## Cómo Funciona

1. **Identificar el código objetivo** — Encontrar la función, clase o módulo a probar
2. **Escribir un spec Kotest** — Elegir un estilo de spec (StringSpec, FunSpec, BehaviorSpec) acorde al alcance de la prueba
3. **Mockear dependencias** — Usar MockK para aislar la unidad bajo prueba
4. **Ejecutar pruebas (ROJO)** — Verificar que la prueba falla con el error esperado
5. **Implementar código (VERDE)** — Escribir el código mínimo para pasar la prueba
6. **Refactorizar** — Mejorar la implementación manteniendo las pruebas en verde
7. **Verificar cobertura** — Ejecutar `./gradlew koverHtmlReport` y verificar 80%+ de cobertura

## Ejemplos

Las siguientes secciones contienen ejemplos detallados y ejecutables para cada patrón de prueba:

### Referencia Rápida

- **Specs Kotest** — Ejemplos de StringSpec, FunSpec, BehaviorSpec, DescribeSpec en [Estilos de Spec Kotest](#estilos-de-spec-kotest)
- **Mocking** — Configuración de MockK, mocking de coroutines, captura de argumentos en [MockK](#mockk)
- **Flujo de trabajo TDD** — Ciclo RED/GREEN/REFACTOR completo con EmailValidator en [Flujo de Trabajo TDD para Kotlin](#flujo-de-trabajo-tdd-para-kotlin)
- **Cobertura** — Configuración de Kover y comandos en [Cobertura con Kover](#cobertura-con-kover)
- **Pruebas Ktor** — Configuración de testApplication en [Pruebas con Ktor testApplication](#pruebas-con-ktor-testapplication)

### Flujo de Trabajo TDD para Kotlin

#### El Ciclo ROJO-VERDE-REFACTORIZAR

```
ROJO        -> Escribir primero una prueba fallida
VERDE       -> Escribir el código mínimo para pasar la prueba
REFACTORIZAR -> Mejorar el código manteniendo las pruebas en verde
REPETIR     -> Continuar con el siguiente requisito
```

#### TDD Paso a Paso en Kotlin

```kotlin
// Paso 1: Definir la interfaz/firma
// EmailValidator.kt
package com.example.validator

fun validateEmail(email: String): Result<String> {
    TODO("not implemented")
}

// Paso 2: Escribir la prueba fallida (ROJO)
// EmailValidatorTest.kt
package com.example.validator

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.result.shouldBeFailure
import io.kotest.matchers.result.shouldBeSuccess

class EmailValidatorTest : StringSpec({
    "valid email returns success" {
        validateEmail("[email protected]").shouldBeSuccess("[email protected]")
    }

    "empty email returns failure" {
        validateEmail("").shouldBeFailure()
    }

    "email without @ returns failure" {
        validateEmail("userexample.com").shouldBeFailure()
    }
})

// Paso 3: Ejecutar pruebas - verificar FALLO
// $ ./gradlew test
// EmailValidatorTest > valid email returns success FAILED
//   kotlin.NotImplementedError: An operation is not implemented

// Paso 4: Implementar el código mínimo (VERDE)
fun validateEmail(email: String): Result<String> {
    if (email.isBlank()) return Result.failure(IllegalArgumentException("Email cannot be blank"))
    if ('@' !in email) return Result.failure(IllegalArgumentException("Email must contain @"))
    val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
    if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Invalid email format"))
    return Result.success(email)
}

// Paso 5: Ejecutar pruebas - verificar PASE
// $ ./gradlew test
// EmailValidatorTest > valid email returns success PASSED
// EmailValidatorTest > empty email returns failure PASSED
// EmailValidatorTest > email without @ returns failure PASSED

// Paso 6: Refactorizar si es necesario, verificar que las pruebas siguen pasando
```

### Estilos de Spec Kotest

#### StringSpec (El Más Simple)

```kotlin
class CalculatorTest : StringSpec({
    "add two positive numbers" {
        Calculator.add(2, 3) shouldBe 5
    }

    "add negative numbers" {
        Calculator.add(-1, -2) shouldBe -3
    }

    "add zero" {
        Calculator.add(0, 5) shouldBe 5
    }
})
```

#### FunSpec (Similar a JUnit)

```kotlin
class UserServiceTest : FunSpec({
    val repository = mockk<UserRepository>()
    val service = UserService(repository)

    test("getUser returns user when found") {
        val expected = User(id = "1", name = "Alice")
        coEvery { repository.findById("1") } returns expected

        val result = service.getUser("1")

        result shouldBe expected
    }

    test("getUser throws when not found") {
        coEvery { repository.findById("999") } returns null

        shouldThrow<UserNotFoundException> {
            service.getUser("999")
        }
    }
})
```

#### BehaviorSpec (Estilo BDD)

```kotlin
class OrderServiceTest : BehaviorSpec({
    val repository = mockk<OrderRepository>()
    val paymentService = mockk<PaymentService>()
    val service = OrderService(repository, paymentService)

    Given("a valid order request") {
        val request = CreateOrderRequest(
            userId = "user-1",
            items = listOf(OrderItem("product-1", quantity = 2)),
        )

        When("the order is placed") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Success
            coEvery { repository.save(any()) } answers { firstArg() }

            val result = service.placeOrder(request)

            Then("it should return a confirmed order") {
                result.status shouldBe OrderStatus.CONFIRMED
            }

            Then("it should charge payment") {
                coVerify(exactly = 1) { paymentService.charge(any()) }
            }
        }

        When("payment fails") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Declined

            Then("it should throw PaymentException") {
                shouldThrow<PaymentException> {
                    service.placeOrder(request)
                }
            }
        }
    }
})
```

#### DescribeSpec (Estilo RSpec)

```kotlin
class UserValidatorTest : DescribeSpec({
    describe("validateUser") {
        val validator = UserValidator()

        context("with valid input") {
            it("accepts a normal user") {
                val user = CreateUserRequest("Alice", "[email protected]")
                validator.validate(user).shouldBeValid()
            }
        }

        context("with invalid name") {
            it("rejects blank name") {
                val user = CreateUserRequest("", "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }

            it("rejects name exceeding max length") {
                val user = CreateUserRequest("A".repeat(256), "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }
        }
    }
})
```

### Matchers de Kotest

#### Matchers Principales

```kotlin
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.*
import io.kotest.matchers.collections.*
import io.kotest.matchers.nulls.*

// Igualdad
result shouldBe expected
result shouldNotBe unexpected

// Strings
name shouldStartWith "Al"
name shouldEndWith "ice"
name shouldContain "lic"
name shouldMatch Regex("[A-Z][a-z]+")
name.shouldBeBlank()

// Colecciones
list shouldContain "item"
list shouldHaveSize 3
list.shouldBeSorted()
list.shouldContainAll("a", "b", "c")
list.shouldBeEmpty()

// Nulls
result.shouldNotBeNull()
result.shouldBeNull()

// Tipos
result.shouldBeInstanceOf<User>()

// Números
count shouldBeGreaterThan 0
price shouldBeInRange 1.0..100.0

// Excepciones
shouldThrow<IllegalArgumentException> {
    validateAge(-1)
}.message shouldBe "Age must be positive"

shouldNotThrow<Exception> {
    valid

Related in General