Communitygithub.com

kotlin-testing

Patrones de pruebas en Kotlin con Kotest, MockK, pruebas de corrutinas, pruebas basadas en propiedades y cobertura Kover. Sigue la metodología TDD con prácticas idiomáticas de Kotlin. Úsalo al escribir pruebas en Kotlin con Kotest o MockK, o al probar corrutinas y verificar cobertura.

¿Qué es kotlin-testing?

Este skill proporciona un conjunto completo de patrones para escribir pruebas en Kotlin de manera confiable y mantenible, siguiendo la metodología TDD. Está diseñado para desarrolladores que trabajan con Kotest y MockK, e incluye guías detalladas para pruebas de corrutinas, pruebas basadas en propiedades y configuración de cobertura de código con Kover. El skill cubre desde la selección del estilo de especificación de Kotest (StringSpec, FunSpec, BehaviorSpec, DescribeSpec) hasta técnicas avanzadas de mocking, captura de argumentos y spies. También ofrece ejemplos prácticos de ciclo RED-GREEN-REFACTOR, pruebas de flujos con `runTest`, y generación de datos aleatorios para pruebas de propiedades con generadores personalizados. Es compatible con herramientas como Gradle y Kover, y es ideal para cualquier proyecto Kotlin que busque calidad y robustez a través de pruebas automatizadas.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/kotlin-testing

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Patrones de Pruebas en Kotlin

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

Cuándo Usarlo

  • Al escribir nuevas funciones o clases en Kotlin
  • Al agregar cobertura de pruebas a código Kotlin existente
  • Al implementar pruebas basadas en propiedades
  • Al seguir el flujo de trabajo TDD en proyectos Kotlin
  • Al configurar Kover para cobertura de código

Cómo Funciona

  1. Identificar el código objetivo — Encuentra la función, clase o módulo a probar
  2. Escribir un spec de Kotest — Elige un estilo de spec (StringSpec, FunSpec, BehaviorSpec) que coincida con el alcance de la prueba
  3. Mockear dependencias — Usa MockK para aislar la unidad bajo prueba
  4. Ejecutar pruebas (RED) — Verifica que la prueba falle con el error esperado
  5. Implementar código (GREEN) — Escribe el código mínimo para que la prueba pase
  6. Refactorizar — Mejora la implementación mientras las pruebas sigan en verde
  7. Verificar cobertura — Ejecuta ./gradlew koverHtmlReport y verifica una cobertura del 80% o más

Ejemplos

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

Referencia Rápida

Flujo de Trabajo TDD para Kotlin

El Ciclo RED-GREEN-REFACTOR

RED     -> Escribe una prueba que falle primero
GREEN   -> Escribe el código mínimo para que la prueba pase
REFACTOR -> Mejora el código mientras las pruebas sigan en verde
REPEAT  -> Continúa con el siguiente requisito

TDD Paso a Paso en Kotlin

// Paso 1: Define la interfaz/firma
// EmailValidator.kt
package com.example.validator

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

// Paso 2: Escribe la prueba que falla (RED)
// 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({
    "email válido devuelve éxito" {
        validateEmail("[email protected]").shouldBeSuccess("[email protected]")
    }

    "email vacío devuelve fallo" {
        validateEmail("").shouldBeFailure()
    }

    "email sin @ devuelve fallo" {
        validateEmail("userexample.com").shouldBeFailure()
    }
})

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

// Paso 4: Implementa el código mínimo (GREEN)
fun validateEmail(email: String): Result<String> {
    if (email.isBlank()) return Result.failure(IllegalArgumentException("El email no puede estar vacío"))
    if ('@' !in email) return Result.failure(IllegalArgumentException("El email debe contener @"))
    val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
    if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Formato de email inválido"))
    return Result.success(email)
}

// Paso 5: Ejecuta las pruebas - verifica PASO
// $ ./gradlew test
// EmailValidatorTest > valid email returns success PASSED
// EmailValidatorTest > empty email returns failure PASSED
// EmailValidatorTest > email without @ returns failure PASSED

// Paso 6: Refactoriza si es necesario, verifica que las pruebas sigan pasando

Estilos de Spec de Kotest

StringSpec (El más simple)

class CalculatorTest : StringSpec({
    "sumar dos números positivos" {
        Calculator.add(2, 3) shouldBe 5
    }

    "sumar números negativos" {
        Calculator.add(-1, -2) shouldBe -3
    }

    "sumar cero" {
        Calculator.add(0, 5) shouldBe 5
    }
})

FunSpec (Estilo JUnit)

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

    test("getUser devuelve usuario cuando se encuentra") {
        val expected = User(id = "1", name = "Alice")
        coEvery { repository.findById("1") } returns expected

        val result = service.getUser("1")

        result shouldBe expected
    }

    test("getUser lanza excepción cuando no se encuentra") {
        coEvery { repository.findById("999") } returns null

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

BehaviorSpec (Estilo BDD)

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

    Given("una solicitud de pedido válida") {
        val request = CreateOrderRequest(
            userId = "user-1",
            items = listOf(OrderItem("product-1", quantity = 2)),
        )

        When("se realiza el pedido") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Success
            coEvery { repository.save(any()) } answers { firstArg() }

            val result = service.placeOrder(request)

            Then("debería devolver un pedido confirmado") {
                result.status shouldBe OrderStatus.CONFIRMED
            }

            Then("debería cobrar el pago") {
                coVerify(exactly = 1) { paymentService.charge(any()) }
            }
        }

        When("el pago falla") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Declined

            Then("debería lanzar PaymentException") {
                shouldThrow<PaymentException> {
                    service.placeOrder(request)
                }
            }
        }
    }
})

DescribeSpec (Estilo RSpec)

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

        context("con entrada válida") {
            it("acepta un usuario normal") {
                val user = CreateUserRequest("Alice", "[email protected]")
                validator.validate(user).shouldBeValid()
            }
        }

        context("con nombre inválido") {
            it("rechaza nombre vacío") {
                val user = CreateUserRequest("", "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }

            it("rechaza nombre que excede la longitud máxima") {
                val user = CreateUserRequest("A".repeat(256), "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }
        }
    }
})

Matchers de Kotest

Matchers Principales

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

// Cadenas
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()

// Nulos
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 "La edad debe ser positiva"

shouldNotThrow<Exception> {
    validateAge(25)
}

Matchers Personalizados

fun beActiveUser() = object : Matcher<User> {
    override fun test(value: User) = MatcherResult(
        value.isActive && value.lastLogin != null,
        { "El usuario ${value.id} debería estar activo con un último inicio de sesión" },
        { "El usuario ${value.id} no debería estar activo" },
    )
}

// Uso
user should beActiveUser()

MockK

Mocking Básico

class UserServiceTest : FunSpec({
    val repository = mockk<UserRepository>()
    val logger = mockk<Logger>(relaxed = true) // Relaxed: devuelve valores por defecto
    val service = UserService(repository, logger)

    beforeTest {
        clearMocks(repository, logger)
    }

    test("findUser delega al repositorio") {
        val expected = User(id = "1", name = "Alice")
        every { repository.findById("1") } returns expected

        val result = service.findUser("1")

        result shouldBe expected
        verify(exactly = 1) { repository.findById("1") }
    }

    test("findUser devuelve null para id desconocido") {
        every { repository.findById(any()) } returns null

        val result = service.findUser("desconocido")

        result.shouldBeNull()
    }
})

Mocking de Corrutinas

class AsyncUserServiceTest : FunSpec({
    val repository = mockk<UserRepository>()
    val service = UserService(repository)

    test("getUser función suspendida") {
        coEvery { repository.findById("1") } returns User(id = "1", name = "Alice")

        val result = service.getUser("1")

        result.name shouldBe "Alice"
        coVerify { repository.findById("1") }
    }

    test("getUser con retardo") {
        coEvery { repository.findById("1") } coAnswers {
            delay(100) // Simula trabajo asíncrono
            User(id = "1", name = "Alice")
        }

        val result = service.getUser("1")
        result.name shouldBe "Alice"
    }
})

Captura de Argumentos

test("save captura el argumento de usuario") {
    val slot = slot<User>()
    coEvery { repository.save(capture(slot)) } returns Unit

    service.createUser(CreateUserRequest("Alice", "[email protected]"))

    slot.captured.name shouldBe "Alice"
    slot.captured.email shouldBe "[email protected]"
    slot.captured.id.shouldNotBeNull()
}

Spy y Mocking Parcial

test("spy sobre objeto real") {
    val realService = UserService(repository)
    val spy = spyk(realService)

    every { spy.generateId() } returns "fixed-id"

    spy.createUser(request)

    verify { spy.generateId() } // Sobrescrito
    // Otros métodos usan implementación real
}

Pruebas de Corrutinas

runTest para Funciones Suspendidas

import kotlinx.coroutines.test.runTest

class CoroutineServiceTest : FunSpec({
    test("búsquedas concurrentes se completan juntas") {
        runTest {
            val service = DataService(testScope = this)

            val result = service.fetchAllData()

            result.users.shouldNotBeEmpty()
            result.products.shouldNotBeEmpty()
        }
    }

    test("tiempo de espera después de retardo") {
        runTest {
            val service = SlowService()

            shouldThrow<TimeoutCancellationException> {
                withTimeout(100) {
                    service.slowOperation() // Toma > 100ms
                }
            }
        }
    }
})

Pruebas de Flujos

import io.kotest.matchers.collections.shouldContainInOrder
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest

class FlowServiceTest : FunSpec({
    test("observeUsers emite actualizaciones") {
        runTest {
            val service = UserFlowService()

            val emissions = service.observeUsers()
                .take(3)
                .toList()

            emissions shouldHaveSize 3
            emissions.last().shouldNotBeEmpty()
        }
    }

    test("searchUsers aplica debounce a la entrada") {
        runTest {
            val service = SearchService()
            val queries = MutableSharedFlow<String>()

            val results = mutableListOf<List<User>>()
            val job = launch {
                service.searchUsers(queries).collect { results.add(it) }
            }

            queries.emit("a")
            queries.emit("ab")
            queries.emit("abc") // Solo esto debería activar la búsqueda
            advanceTimeBy(500)

            results shouldHaveSize 1
            job.cancel()
        }
    }
})

TestDispatcher

import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle

class DispatcherTest : FunSpec({
    test("usa test dispatcher para ejecución controlada") {
        val dispatcher = StandardTestDispatcher()

        runTest(dispatcher) {
            var completado = false

            launch {
                delay(1000)
                completado = true
            }

            completado shouldBe false
            advanceTimeBy(1000)
            completado shouldBe true
        }
    }
})

Pruebas Basadas en Propiedades

Pruebas de Propiedades con Kotest

import io.kotest.core.spec.style.FunSpec
import io.kotest.property.Arb
import io.kotest.property.arbitrary.*
import io.kotest.property.forAll
import io.kotest.property.checkAll
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString

// Nota: La prueba de ida y vuelta de serialización a continuación requiere que la clase de datos User
// esté anotada con @Serializable (de kotlinx.serialization).

class PropertyTest : FunSpec({
    test("invertir cadena es involutivo") {
        forAll<String> { s ->
            s.reversed().reversed() == s
        }
    }

    test("ordenar lista es idempotente") {
        forAll(Arb.list(Arb.int())) { list ->
            list.sorted() == list.sorted().sorted()
        }
    }

    test("ida y vuelta de serialización preserva datos") {
        checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->
            User(name = name, email = "$email@test.com")
        }) { user ->
            val json = Json.encodeToString(user)
            val decoded = Json.decodeFromString<User>(json)
            decoded shouldBe user
        }
    }
})

Generadores Personalizados

import io.kotest.property.Arb
import io.kotest.property.arbitrary.*

// Generador personalizado para emails válidos
fun Arb.Companion.email(
    localPartSize: IntRange = 1..20,
    domainSize: IntRange = 1..10
): Arb<String> = arb {
    val localPart = Arb.string(localPartSize, Codepoint.alphanumeric()).bind()
    val domain = Arb.string(domainSize, Codepoint.alphanumeric()).bind()
    val tld = Arb.of("com", "org", "net", "io").bind()
    "${localPart}@${domain}.${tld}"
}

// Generador de User aleatorio
fun Arb.Companion.user(): Arb<User> = arb {
    val id = Arb.uuid().bind().toString()
    val name = Arb.string(3..30).bind()
    val email = Arb.email().bind()
    val isActive = Arb.boolean().bind()
    User(id = id, name = name, email = email, isActive = isActive)
}

// Uso en prueba
test("usuarios generados aleatoriamente tienen emails válidos") {
    forAll(Arb.user()) { user ->
        user.email.matches(Regex("^[\\w.-]+@[\\w.-]+\\.\\w+$"))
    }
}

Cobertura Kover

Configuración de Gradle

// build.gradle.kts
plugins {
    kotlin("jvm")
    id("org.jetbrains.kotlinx.kover") version "0.8.0"
}

dependencies {
    testImplementation("io.kotest:kotest-runner-junit5:5.9.0")
    testImplementation("io.kotest:kotest-assertions-core:5.9.0")
    testImplementation("io.kotest:kotest-property:5.9.0")
    testImplementation("io.mockk:mockk:1.13.12")
}

kover {
    reports {
        filters {
            excludes {
                classes(
                    "*Application*",
                    "*Config*",
                    "*DTO*"
                )
            }
        }
        verify {
            rule {
                minBound(80)
            }
        }
    }
}

Comandos de Cobertura

# Ejecutar pruebas con cobertura
./gradlew koverHtmlReport
# Informe en build/reports/kover/html/index.html

# Verificar cobertura mínima
./gradlew koverVerify
# Falla si la cobertura < 80%

# Informe XML (para CI/CD)
./gradlew koverXmlReport
# Informe en build/reports/kover/xml/report.xml

# Informe de cobertura por paquete
./gradlew koverHtmlReport --kotlin-project :app
# Informe específico del proyecto

Ejemplo de Verificación en CI

# .github/workflows/test.yml
name: Pruebas y Cobertura
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '21'
      - run: ./gradlew test koverVerify

Pruebas con testApplication de Ktor

Configuración Básica

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.testApplication

class UserRoutesTest : FunSpec({
    test("GET /users devuelve lista") {
        testApplication {
            application {
                module() // Tu módulo principal de Ktor
            }

            val response = client.get("/users")

            response.status shouldBe HttpStatusCode.OK
            response.bodyAsText() shouldContain "users"
        }
    }

    test("POST /users crea usuario") {
        testApplication {
            application {
                module()
            }

            val response = client.post("/users") {
                contentType(ContentType.Application.Json)
                setBody(CreateUserRequest("Alice", "[email protected]"))
            }

            response.status shouldBe HttpStatusCode.Created
        }
    }

    test("GET /users/:id devuelve 404 para usuario inexistente") {
        testApplication {
            application {
                module()
            }

            val response = client.get("/users/unknown-id")

            response.status shouldBe HttpStatusCode.NotFound
        }
    }
})

Pruebas de Autenticación

test("ruta protegida requiere token") {
    testApplication {
        application {
            module()
        }

        val response = client.get("/admin/dashboard")

        response.status shouldBe HttpStatusCode.Unauthorized
    }
}

test("ruta protegida con token válido") {
    testApplication {
        application {
            module()
        }

        val token = generateTestToken()
        val response = client.get("/admin/dashboard") {
            bearerAuth(token)
        }

        response.status shouldBe HttpStatusCode.OK
    }
}

Mejores Prácticas

  1. Usar los estilos de spec adecuados — StringSpec para pruebas simples, BehaviorSpec para BDD, DescribeSpec para pruebas de unidad jerárquicas
  2. Mockear por interfaz — Siempre mockear por interfaz, no clases concretas
  3. MockK vs Mockito — Preferir MockK sobre Mockito para proyectos Kotlin
  4. runTest para corrutinas — Usar runTest en lugar de runBlocking para pruebas de corrutinas
  5. Pruebas basadas en propiedades — Usar forAll en lugar de ejemplos manuales para cobertura más amplia
  6. Cobertura por rama — Apuntar a > 80% de cobertura de rama (no solo de línea)
  7. Aislar archivos de prueba — Una clase de prueba por archivo de producción
  8. Nomenclatura — Nombrar los archivos de prueba como {ClassName}Test.kt o {ClassName}Spec.kt

Comandos

# Ejecutar todas las pruebas
./gradlew test

# Ejecutar clases de prueba específicas
./gradlew test --tests "*UserServiceTest"

# Ejecutar prueba individual (JUnit5)
./gradlew test --tests "*UserServiceTest.should create user*"

# Ver cobertura
./gradlew koverHtmlReport

# Verificar cobertura mínima
./gradlew koverVerify

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

Skills relacionados