Communitygithub.com

kotlin-testing

Padrões de teste em Kotlin com Kotest, MockK, testes de corrotinas, teste baseado em propriedades e cobertura Kover. Segue a metodologia TDD com práticas idiomáticas de Kotlin. Use ao escrever testes Kotlin com Kotest ou MockK, ou ao testar corrotinas e verificar cobertura.

O que é kotlin-testing?

Este skill fornece um conjunto abrangente de padrões de teste Kotlin para escrever testes confiáveis e sustentáveis, seguindo a metodologia TDD com Kotest e MockK. Ele é útil quando você precisa escrever novas funções ou classes Kotlin, adicionar cobertura de teste a código existente, implementar testes baseados em propriedades, ou seguir o fluxo de trabalho TDD em projetos Kotlin. O skill abrange desde a identificação do código alvo e escrita de uma especificação Kotest até a configuração da cobertura de código com Kover. Ele suporta plataformas como Kotlin/JVM, Kotlin Multiplatform e projetos Gradle, fornecendo exemplos detalhados para cada padrão de teste, incluindo estilos de especificação Kotest (StringSpec, FunSpec, BehaviorSpec, DescribeSpec), mocking com MockK, testes de corrotinas, teste baseado em propriedades e verificação de cobertura. O funcionamento segue as etapas: identificar o código alvo, escrever um teste falho (RED), implementar o código mínimo para passar (GREEN), refatorar e verificar a cobertura (REFACTOR).

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Padrões de Teste Kotlin

Padrões abrangentes de teste Kotlin para escrever testes confiáveis e sustentáveis seguindo a metodologia TDD com Kotest e MockK.

Quando Usar

  • Escrevendo novas funções ou classes Kotlin
  • Adicionando cobertura de teste a código Kotlin existente
  • Implementando testes baseados em propriedades
  • Seguindo o fluxo de trabalho TDD em projetos Kotlin
  • Configurando Kover para cobertura de código

Como Funciona

  1. Identifique o código alvo — Encontre a função, classe ou módulo a ser testado
  2. Escreva uma especificação Kotest — Escolha um estilo de especificação (StringSpec, FunSpec, BehaviorSpec) adequado ao escopo do teste
  3. Simule dependências — Use MockK para isolar a unidade sob teste
  4. Execute testes (VERMELHO) — Verifique se o teste falha com o erro esperado
  5. Implemente o código (VERDE) — Escreva o código mínimo para passar no teste
  6. Refatore — Melhore a implementação mantendo os testes verdes
  7. Verifique a cobertura — Execute ./gradlew koverHtmlReport e verifique cobertura acima de 80%

Exemplos

As seções a seguir contêm exemplos detalhados e executáveis para cada padrão de teste:

Referência Rápida

Fluxo de Trabalho TDD para Kotlin

O Ciclo VERMELHO-VERDE-REFATORAR

VERMELHO  -> Escreva um teste falho primeiro
VERDE     -> Escreva código mínimo para passar no teste
REFATORAR -> Melhore o código mantendo os testes verdes
REPITA    -> Continue com o próximo requisito

Passo a Passo TDD em Kotlin

// Passo 1: Defina a interface/assinatura
// EmailValidator.kt
package com.example.validator

fun validateEmail(email: String): Result<String> {
    TODO("não implementado")
}

// Passo 2: Escreva teste falho (VERMELHO)
// 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 retorna sucesso" {
        validateEmail("[email protected]").shouldBeSuccess("[email protected]")
    }

    "email vazio retorna falha" {
        validateEmail("").shouldBeFailure()
    }

    "email sem @ retorna falha" {
        validateEmail("userexample.com").shouldBeFailure()
    }
})

// Passo 3: Execute testes - verifique FALHA
// $ ./gradlew test
// EmailValidatorTest > valid email returns success FALHOU
//   kotlin.NotImplementedError: Uma operação não foi implementada

// Passo 4: Implemente código mínimo (VERDE)
fun validateEmail(email: String): Result<String> {
    if (email.isBlank()) return Result.failure(IllegalArgumentException("Email não pode estar em branco"))
    if ('@' !in email) return Result.failure(IllegalArgumentException("Email deve conter @"))
    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)
}

// Passo 5: Execute testes - verifique PASSAGEM
// $ ./gradlew test
// EmailValidatorTest > valid email returns success PASSADO
// EmailValidatorTest > empty email returns failure PASSADO
// EmailValidatorTest > email without @ returns failure PASSADO

// Passo 6: Refatore se necessário, verifique se os testes ainda passam

Estilos de Especificação Kotest

StringSpec (Mais Simples)

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

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

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

FunSpec (Estilo JUnit)

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

    test("getUser retorna usuário quando encontrado") {
        val expected = User(id = "1", name = "Alice")
        coEvery { repository.findById("1") } returns expected

        val result = service.getUser("1")

        result shouldBe expected
    }

    test("getUser lança exceção quando não encontrado") {
        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("uma requisição de pedido válida") {
        val request = CreateOrderRequest(
            userId = "user-1",
            items = listOf(OrderItem("product-1", quantity = 2)),
        )

        When("o pedido é realizado") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Success
            coEvery { repository.save(any()) } answers { firstArg() }

            val result = service.placeOrder(request)

            Then("deve retornar um pedido confirmado") {
                result.status shouldBe OrderStatus.CONFIRMED
            }

            Then("deve cobrar o pagamento") {
                coVerify(exactly = 1) { paymentService.charge(any()) }
            }
        }

        When("o pagamento falha") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Declined

            Then("deve lançar PaymentException") {
                shouldThrow<PaymentException> {
                    service.placeOrder(request)
                }
            }
        }
    }
})

DescribeSpec (Estilo RSpec)

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

        context("com entrada válida") {
            it("aceita um usuário normal") {
                val user = CreateUserRequest("Alice", "[email protected]")
                validator.validate(user).shouldBeValid()
            }
        }

        context("com nome inválido") {
            it("rejeita nome em branco") {
                val user = CreateUserRequest("", "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }

            it("rejeita nome excedendo o tamanho máximo") {
                val user = CreateUserRequest("A".repeat(256), "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }
        }
    }
})

Matchers do Kotest

Matchers Principais

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.*

// Igualdade
result shouldBe expected
result shouldNotBe unesperado

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

// Coleções
lista shouldContain "item"
lista shouldHaveSize 3
lista.shouldBeSorted()
lista.shouldContainAll("a", "b", "c")
lista.shouldBeEmpty()

// Nulos
resultado.shouldNotBeNull()
resultado.shouldBeNull()

// Tipos
resultado.shouldBeInstanceOf<User>()

// Números
contagem shouldBeGreaterThan 0
preco shouldBeInRange 1.0..100.0

// Exceções
shouldThrow<IllegalArgumentException> {
    validarIdade(-1)
}.message shouldBe "Idade deve ser positiva"

shouldNotThrow<Exception> {
    validarIdade(25)
}

Matchers Personalizados

fun serUsuarioAtivo() = object : Matcher<User> {
    override fun test(value: User) = MatcherResult(
        value.isActive && value.lastLogin != null,
        { "Usuário ${value.id} deve estar ativo com um último login" },
        { "Usuário ${value.id} não deve estar ativo" },
    )
}

// Uso
usuario should serUsuarioAtivo()

MockK

Simulação Básica

class UserServiceTest : FunSpec({
    val repository = mockk<UserRepository>()
    val logger = mockk<Logger>(relaxed = true) // Relaxado: retorna valores padrão
    val service = UserService(repository, logger)

    beforeTest {
        clearMocks(repository, logger)
    }

    test("findUser delega ao repositório") {
        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 retorna null para id desconhecido") {
        every { repository.findById(any()) } returns null

        val result = service.findUser("desconhecido")

        result.shouldBeNull()
    }
})

Simulação de Corrotinas

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

    test("getUser função suspensa") {
        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 com atraso") {
        coEvery { repository.findById("1") } coAnswers {
            delay(100) // Simula trabalho assíncrono
            User(id = "1", name = "Alice")
        }

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

Captura de Argumentos

test("save captura o argumento do usuário") {
    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 e Simulação Parcial

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

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

    spy.createUser(request)

    verify { spy.generateId() } // Sobrescrito
    // Outros métodos usam implementação real
}

Teste de Corrotinas

runTest para Funções Suspensas

import kotlinx.coroutines.test.runTest

class CoroutineServiceTest : FunSpec({
    test("buscas concorrentes concluem juntas") {
        runTest {
            val service = DataService(testScope = this)

            val result = service.fetchAllData()

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

    test("timeout após atraso") {
        runTest {
            val service = SlowService()

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

Testando Flows

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 atualizações") {
        runTest {
            val service = UserFlowService()

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

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

    test("searchUsers filtra entrada com debounce") {
        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") // Apenas isso deve acionar a busca
            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 execução controlada") {
        val dispatcher = StandardTestDispatcher()

        runTest(dispatcher) {
            var completed = false

            launch {
                delay(1000)
                completed = true
            }

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

Teste Baseado em Propriedades

Teste de Propriedades 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: O teste de ida e volta de serialização abaixo requer que a classe de dados User
// seja anotada com @Serializable (do kotlinx.serialization).

class PropertyTest : FunSpec({
    test("reverter string é involutivo") {
        forAll<String> { s ->
            s.reversed().reversed() == s
        }
    }

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

    test("ida e volta de serialização preserva dados") {
        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
        }
    }
})

Geradores Personalizados


...

[truncado para tradução]

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.

Habilidades Relacionadas