Resposta rápida: use Kotest com Kotlin quando você quer testes mais legíveis, matchers idiomáticos e geração automática de casos com property-based testing. O framework roda integrado ao Gradle e à plataforma JUnit, oferece estilos como StringSpec, FunSpec e BehaviorSpec e pode conviver com MockK, Testcontainers e kotlinx-coroutines-test. Para uma adoção segura, comece com Kotest Runner + Assertions, mantenha um único estilo por módulo e introduza testes baseados em propriedades apenas nas regras com invariantes claros.
Kotest não é apenas uma sintaxe diferente para @Test. Seu principal valor aparece quando o domínio possui muitas combinações de entrada: valores monetários, intervalos, parsers, validações, ordenação, serialização e regras que deveriam continuar verdadeiras para centenas de exemplos. Em vez de escrever manualmente cinco casos, você descreve uma propriedade e deixa o framework explorar o espaço de dados.
Este guia mostra o setup, compara os estilos de especificação, cria testes tradicionais e property-based, integra coroutines e MockK e explica quando continuar com JUnit 5 puro.
O que é Kotest?
Kotest é um framework de testes criado para Kotlin. Ele reúne quatro grupos de recursos:
- test framework, com diferentes estilos para organizar casos;
- assertions, com matchers como
shouldBe,shouldContaineshouldThrow; - property testing, com geradores de dados e redução automática de falhas;
- integrações, incluindo extensões para Spring, Testcontainers e outros componentes do ecossistema JVM.
O runner se conecta à plataforma JUnit, portanto IDEs e pipelines que já descobrem testes JUnit normalmente também conseguem executar specs do Kotest. Isso permite uma migração gradual: uma base pode manter classes JUnit 5 existentes e criar novos testes com Kotest sem reescrever tudo de uma vez.
Se sua necessidade imediata é aprender mocks, lifecycle e testes parametrizados convencionais, comece pelo guia de JUnit 5 e MockK com Kotlin. Kotest entra como uma camada mais Kotlin-first para expressão e geração de casos.
Kotest ou JUnit 5: qual escolher?
Kotest e JUnit 5 não precisam ser tratados como rivais absolutos. O Kotest usa a infraestrutura da plataforma JUnit em muitos projetos, enquanto oferece uma API própria para escrever as especificações.
| Critério | Kotest | JUnit 5 puro |
|---|---|---|
| Organização | Vários estilos de spec | Classes e métodos com anotações |
| Assertions | Matchers idiomáticos integrados | Assertions básicas ou bibliotecas externas |
| Property-based testing | Módulo oficial e integrado | Exige biblioteca adicional |
| Curva de aprendizado | Maior se cada time adotar um estilo diferente | Familiar para equipes Java/JVM |
| Interoperabilidade Java | Melhor em bases Kotlin-first | Excelente em bases Java e Kotlin |
| Migração | Pode coexistir com JUnit | Já costuma ser o padrão da base |
| Melhor encaixe | Domínio Kotlin, DSLs, invariantes e testes expressivos | Convenção corporativa, testes simples e equipes mistas |
Escolha Kotest quando legibilidade Kotlin-first e property testing trazem ganho real. Fique com JUnit 5 quando o time já possui um padrão estável, a maioria dos testes é simples ou a base é compartilhada com muitas pessoas desenvolvedoras Java. Adicionar um framework sem resolver um problema concreto só aumenta o custo cognitivo.
Configurando Kotest no Gradle Kotlin DSL
Consulte as versões compatíveis na documentação oficial de instalação e centralize-as no version catalog ou no gerenciamento de dependências do projeto. O setup mínimo inclui o runner para JUnit 5 e o módulo de assertions.
// build.gradle.kts
dependencies {
testImplementation("io.kotest:kotest-runner-junit5:<versao-atual>")
testImplementation("io.kotest:kotest-assertions-core:<versao-atual>")
testImplementation("io.kotest:kotest-property:<versao-atual>")
}
tasks.test {
useJUnitPlatform()
}
Usar <versao-atual> aqui é intencional: copie uma versão estável compatível com seu Kotlin, Gradle e JDK, em vez de reproduzir cegamente um número que pode estar desatualizado. Em um projeto real, fixe a versão para tornar a build reproduzível.
Com version catalog, a configuração pode ficar assim:
# gradle/libs.versions.toml
[versions]
kotest = "<versao-atual>"
[libraries]
kotest-runner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
// build.gradle.kts
dependencies {
testImplementation(libs.kotest.runner)
testImplementation(libs.kotest.assertions)
testImplementation(libs.kotest.property)
}
Essa organização evita versões divergentes entre módulos. Para revisar plugins, tasks e catálogos, veja o guia de Gradle com Kotlin.
Primeiro teste com StringSpec
StringSpec é um bom ponto de partida porque sua estrutura é direta: cada string representa o nome de um caso.
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CalculadoraFreteTest : StringSpec({
"frete deve ser grátis para pedidos a partir de 200 reais" {
calcularFrete(total = 200.0) shouldBe 0.0
}
"frete deve custar 19,90 para pedidos abaixo do limite" {
calcularFrete(total = 199.99) shouldBe 19.90
}
})
fun calcularFrete(total: Double): Double =
if (total >= 200.0) 0.0 else 19.90
O operador shouldBe lê naturalmente e produz mensagens de falha úteis. O Kotest possui matchers para coleções, strings, exceções, datas, tipos e valores numéricos.
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.string.shouldStartWith
"deve validar e ordenar os nomes" {
val nomes = listOf("Caio", "Ana", "Bruna").sorted()
nomes shouldContainExactly listOf("Ana", "Bruna", "Caio")
nomes.first() shouldStartWith "A"
}
"deve rejeitar total negativo" {
shouldThrow<IllegalArgumentException> {
require(-10.0 >= 0) { "total inválido" }
}
}
StringSpec, FunSpec ou BehaviorSpec?
Kotest oferece vários estilos. Isso é poderoso, mas pode virar desorganização se cada arquivo usar uma DSL diferente sem motivo.
StringSpec para testes objetivos
Use quando cada caso cabe em um nome claro e não precisa de muitos níveis de contexto. É simples para services, mappers, validators e funções de domínio.
FunSpec para uma estrutura familiar
FunSpec usa blocos test e context, ficando próximo de frameworks tradicionais.
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
class CupomServiceTest : FunSpec({
context("cupom percentual") {
test("deve aplicar dez por cento") {
aplicarDesconto(100.0, 10) shouldBe 90.0
}
test("não deve gerar valor negativo") {
aplicarDesconto(20.0, 200) shouldBe 0.0
}
}
})
fun aplicarDesconto(total: Double, percentual: Int): Double =
(total * (1 - percentual / 100.0)).coerceAtLeast(0.0)
BehaviorSpec para comportamento de negócio
BehaviorSpec organiza o teste com Given, When e Then. Ele ajuda quando produto, QA e desenvolvimento conversam usando cenários de negócio.
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
class LimiteCreditoTest : BehaviorSpec({
given("um cliente com limite disponível de 1.000 reais") {
val conta = ContaCredito(limiteDisponivel = 1_000)
`when`("uma compra de 400 reais é autorizada") {
val autorizada = conta.comprar(400)
then("a compra deve ser aprovada") {
autorizada shouldBe true
}
then("o limite restante deve ser 600 reais") {
conta.limiteDisponivel shouldBe 600
}
}
}
})
class ContaCredito(var limiteDisponivel: Int) {
fun comprar(valor: Int): Boolean {
if (valor <= 0 || valor > limiteDisponivel) return false
limiteDisponivel -= valor
return true
}
}
A regra mais importante não é escolher o “melhor” estilo. É escolher um estilo principal por módulo ou contexto. Uma base com cinco DSLs exige que cada pessoa reaprenda a navegar pelos testes.
O que é property-based testing?
Em um teste baseado em exemplos, você escolhe entradas específicas:
normalizar(" Kotlin ") shouldBe "kotlin"
normalizar("KOTLIN") shouldBe "kotlin"
Em property-based testing, você declara uma regra que deve valer para um conjunto amplo de entradas. O Kotest gera os dados, executa várias iterações e informa um contraexemplo quando encontra falha.
Uma propriedade da normalização pode ser: “normalizar duas vezes produz o mesmo resultado que normalizar uma vez”. Isso é chamado de idempotência.
import io.kotest.core.spec.style.StringSpec
import io.kotest.property.Arb
import io.kotest.property.arbitrary.string
import io.kotest.property.checkAll
class NormalizacaoPropertyTest : StringSpec({
"normalização deve ser idempotente" {
checkAll(Arb.string()) { texto ->
val umaVez = normalizar(texto)
val duasVezes = normalizar(umaVez)
duasVezes shouldBe umaVez
}
}
})
fun normalizar(valor: String): String =
valor.trim().lowercase()
O objetivo não é gerar números aleatórios por diversão. Uma boa propriedade expressa uma verdade do domínio.
Criando propriedades úteis
Há padrões que ajudam a encontrar propriedades melhores.
Ida e volta
Serializar e desserializar deveria preservar o valor:
"encode e decode devem preservar o identificador" {
checkAll(Arb.long(1L..Long.MAX_VALUE)) { id ->
val codificado = id.toString(36)
val decodificado = codificado.toLong(36)
decodificado shouldBe id
}
}
Limites ou invariantes
Um desconto nunca deveria aumentar o total nem produzir preço negativo:
import io.kotest.property.arbitrary.double
import io.kotest.property.arbitrary.int
import io.kotest.matchers.doubles.shouldBeAtMost
import io.kotest.matchers.doubles.shouldBeAtLeast
"desconto deve manter resultado entre zero e o total" {
checkAll(
Arb.double(0.0..1_000_000.0),
Arb.int(0..100)
) { total, percentual ->
val resultado = aplicarDesconto(total, percentual)
resultado.shouldBeAtLeast(0.0)
resultado.shouldBeAtMost(total)
}
}
Operação inversa
Adicionar e depois remover o mesmo elemento deveria recuperar o estado anterior, desde que a estrutura e a regra permitam isso.
Relação com uma implementação simples
Uma implementação otimizada pode ser comparada a uma versão lenta, porém obviamente correta, em entradas geradas.
Essas propriedades costumam encontrar casos de borda que exemplos felizes não cobrem: string vazia, espaços Unicode, zero, números muito grandes, listas duplicadas e limites de intervalo.
Geradores com Arb
Arb representa geradores de valores. Além dos tipos básicos, você pode combinar geradores para produzir objetos do domínio.
import io.kotest.property.arbitrary.arbitrary
import io.kotest.property.arbitrary.email
import io.kotest.property.arbitrary.long
import io.kotest.property.arbitrary.stringPattern
data class Usuario(
val id: Long,
val nome: String,
val email: String
)
val usuarioArb = arbitrary {
Usuario(
id = Arb.long(1L..1_000_000L).bind(),
nome = Arb.stringPattern("[A-Z][a-z]{2,12}").bind(),
email = Arb.email().bind()
)
}
Geradores de domínio devem produzir dados válidos por padrão. Quando o objetivo é testar validação, crie geradores separados para entradas válidas e inválidas. Misturar tudo em um gerador torna a intenção difícil de entender.
import io.kotest.matchers.longs.shouldBeGreaterThan
"usuário gerado deve manter contrato de cadastro" {
checkAll(usuarioArb) { usuario ->
usuario.id shouldBeGreaterThan 0L
usuario.nome.isNotBlank() shouldBe true
usuario.email.contains("@") shouldBe true
}
}
Shrinking: como Kotest reduz uma falha
Quando um gerador encontra um erro, mostrar uma entrada gigantesca nem sempre ajuda. Shrinking é o processo de reduzir o caso que falhou para um contraexemplo menor.
Imagine que uma função que divide valores falha somente quando o divisor é zero. O teste pode encontrar o erro depois de gerar muitos números, mas o relatório ideal destaca o caso mínimo relevante, como (1, 0), e não uma combinação enorme.
Essa redução é uma das vantagens práticas do property testing sobre loops com Random. Um loop manual pode reproduzir mal a falha, não registra seed adequadamente e não tenta simplificar a entrada. Kotest oferece infraestrutura específica para geração, classificação e diagnóstico.
Mesmo assim, preserve o contraexemplo como teste de regressão quando ele representar uma regra importante. O property test protege a classe inteira de entradas; o exemplo documenta o bug que realmente aconteceu.
Testando coroutines com Kotest
Specs do Kotest aceitam funções suspensas, o que combina naturalmente com kotlinx-coroutines-test. Use runTest quando precisar controlar tempo virtual, dispatchers e tarefas pendentes.
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
class PerfilServiceTest : StringSpec({
"deve carregar o perfil" {
runTest {
val gateway = PerfilGatewayFake()
val service = PerfilService(gateway)
service.carregar(42) shouldBe Perfil(42, "Karina")
}
}
})
data class Perfil(val id: Long, val nome: String)
class PerfilGatewayFake {
suspend fun buscar(id: Long): Perfil {
delay(1_000)
return Perfil(id, "Karina")
}
}
class PerfilService(private val gateway: PerfilGatewayFake) {
suspend fun carregar(id: Long): Perfil = gateway.buscar(id)
}
Evite Thread.sleep em testes assíncronos. Ele aumenta o tempo da suíte e cria flakiness. Para Flow, combine o Kotest com as ferramentas de coroutines ou use Turbine; o guia de testes de Flow e StateFlow com Turbine mostra cancelamento, emissões e estado inicial.
Kotest com MockK
Kotest Assertions e MockK funcionam bem juntos. O primeiro organiza e verifica resultados; o segundo controla colaboradores quando um fake simples não é suficiente.
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
class PedidoServiceTest : FunSpec({
test("deve consultar e devolver o pedido") {
val repository = mockk<PedidoRepository>()
val esperado = Pedido(id = 10, status = "PAGO")
coEvery { repository.buscar(10) } returns esperado
val service = PedidoService(repository)
val resultado = service.detalhar(10)
resultado shouldBe esperado
coVerify(exactly = 1) { repository.buscar(10) }
}
})
data class Pedido(val id: Long, val status: String)
interface PedidoRepository {
suspend fun buscar(id: Long): Pedido
}
class PedidoService(private val repository: PedidoRepository) {
suspend fun detalhar(id: Long): Pedido = repository.buscar(id)
}
Não transforme todo colaborador em mock. Fakes são mais legíveis para repositórios em memória e objetos determinísticos. Mocks são úteis para falhas controladas, APIs difíceis de instanciar e verificação de interações relevantes. Testar cada chamada interna deixa a suíte acoplada à implementação.
Lifecycle e isolamento entre testes
Kotest permite configurar o ciclo de vida das instâncias e usar hooks como beforeTest, afterTest, beforeSpec e afterSpec. O ponto crítico é impedir que estado mutável de um caso vaze para outro.
class CarrinhoTest : FunSpec({
lateinit var carrinho: Carrinho
beforeTest {
carrinho = Carrinho()
}
test("deve começar vazio") {
carrinho.totalItens() shouldBe 0
}
test("deve contar item adicionado") {
carrinho.adicionar("Livro")
carrinho.totalItens() shouldBe 1
}
})
Se a spec compartilha banco, servidor ou container, documente o escopo e limpe o estado lógico. Para PostgreSQL e outras dependências reais, consulte o guia de Testcontainers com Kotlin.
Testes orientados a dados
Nem toda tabela de exemplos precisa virar property test. Quando os casos são poucos, conhecidos e semanticamente importantes, data-driven testing é mais claro.
import io.kotest.datatest.withData
class ClassificadorTest : FunSpec({
context("classificação por nota") {
withData(
nameFn = { "nota ${it.nota} deve resultar em ${it.resultado}" },
CasoNota(0, "REPROVADO"),
CasoNota(59, "REPROVADO"),
CasoNota(60, "APROVADO"),
CasoNota(100, "APROVADO")
) { caso ->
classificar(caso.nota) shouldBe caso.resultado
}
}
})
data class CasoNota(val nota: Int, val resultado: String)
fun classificar(nota: Int): String =
if (nota >= 60) "APROVADO" else "REPROVADO"
Use uma tabela para limites escolhidos pelo negócio. Use property testing para explorar um intervalo amplo e provar invariantes. Os dois estilos se complementam.
Kotest em Android
Em projetos Android, separe testes locais da JVM e testes instrumentados. Kotest se encaixa melhor nos testes locais de domínio, ViewModel, reducers, mappers e componentes que não dependem diretamente do runtime Android.
Para código que exige dispositivo, emulator, Context, recursos ou componentes do framework, verifique o suporte da ferramenta e da versão adotada pelo projeto. Muitas equipes mantêm JUnit/AndroidX Test nos testes instrumentados e usam Kotest nos módulos Kotlin puros. Essa divisão é pragmática e evita forçar uma única DSL em ambientes com runners diferentes.
Uma arquitetura com domínio desacoplado aumenta a quantidade de código testável na JVM. O conteúdo de modularização Android com Kotlin mostra como separar responsabilidades e reduzir dependências de framework.
Erros comuns ao adotar Kotest
Usar todos os estilos ao mesmo tempo
A variedade parece atraente, mas prejudica a navegação. Defina um estilo padrão e aceite exceções apenas quando a estrutura do teste realmente pedir.
Escrever property tests sem uma propriedade
Gerar dados e repetir a mesma assertion de um exemplo não produz automaticamente um bom teste. Nomeie a regra: idempotência, limite, ida e volta, ordenação, equivalência ou preservação.
Gerar dados inválidos sem intenção
Se o domínio exige identificadores positivos, um gerador genérico de Long vai gastar iterações em entradas irrelevantes. Modele geradores válidos e crie cenários inválidos separadamente.
Ignorar reprodutibilidade
Quando uma falha baseada em geração aparecer, registre seed, contraexemplo e versão das dependências no diagnóstico. Não tente “resolver” diminuindo silenciosamente o número de iterações.
Misturar teste unitário e integração
Uma spec que sobe Spring, banco e fila não é unitária só porque usa uma DSL concisa. Separe tarefas, tags ou módulos para manter feedback rápido.
Testar detalhes internos
Assertions elegantes não justificam acoplamento. Teste comportamento observável, não a sequência completa de métodos privados e chamadas internas.
Estratégia de adoção para um time
Uma migração de baixo risco pode seguir esta ordem:
- adicione Kotest Runner e Assertions em um módulo Kotlin;
- escolha
FunSpecouStringSpeccomo padrão; - converta apenas testes novos ou arquivos que já precisam de manutenção;
- crie matchers de domínio para assertions repetidas;
- introduza property testing em parsers, cálculos e validações;
- monitore tempo de execução e legibilidade nas revisões;
- documente lifecycle, paralelismo e convenções no repositório;
- mantenha JUnit 5 onde a migração não traz retorno.
O objetivo não é aumentar a porcentagem de arquivos “em Kotest”. O objetivo é tornar defeitos mais fáceis de encontrar e testes mais fáceis de manter.
Checklist de um bom setup
- versões centralizadas e compatíveis com Kotlin, Gradle e JDK;
-
useJUnitPlatform()habilitado; - um estilo de spec principal por módulo;
- estado isolado entre casos;
- coroutines testadas com ferramentas de tempo virtual;
- mocks usados apenas quando agregam valor;
- geradores representam o domínio;
- properties possuem nomes e invariantes claros;
- contraexemplos importantes viram regressões explícitas;
- testes unitários e de integração rodam em tarefas identificáveis;
- a suíte funciona igual localmente e no CI.
Perguntas frequentes
Kotest substitui JUnit 5?
Não necessariamente. Em muitos projetos, Kotest usa a plataforma JUnit para descoberta e execução. Você pode manter testes JUnit 5 e adicionar specs Kotest gradualmente. A decisão deve considerar legibilidade, convenção do time e necessidade de property testing.
Kotest funciona com MockK?
Sim. Use MockK para criar mocks e controlar chamadas suspensas com coEvery e coVerify, enquanto Kotest organiza a spec e fornece matchers. Também é possível combinar Kotest com fakes, Testcontainers e ferramentas de teste de coroutines.
Property-based testing substitui testes de exemplo?
Não. Property tests exploram muitas entradas e verificam invariantes; testes de exemplo documentam casos de negócio específicos. Uma suíte forte usa ambos: propriedades para cobertura ampla e exemplos para cenários importantes e regressões conhecidas.
Qual estilo do Kotest é melhor?
StringSpec é simples para casos objetivos, FunSpec oferece uma estrutura familiar e BehaviorSpec funciona bem para cenários de negócio. A consistência do módulo importa mais que a escolha universal de uma DSL.
Kotest é indicado para Android?
É especialmente útil em testes locais da JVM e módulos Kotlin puros. Para testes instrumentados, valide o runner e as integrações adotadas pela versão do projeto. Manter Kotest no domínio e AndroidX Test na instrumentação pode ser uma divisão saudável.
Conclusão
Kotest torna testes Kotlin mais expressivos sem exigir uma migração radical. O ganho inicial vem de specs legíveis e assertions idiomáticas; o diferencial mais profundo aparece no property-based testing, que transforma invariantes do domínio em verificações executadas contra muitas entradas.
Comece pequeno: configure runner e assertions, escolha um estilo e converta uma regra importante. Depois, identifique funções com propriedades claras — normalização idempotente, serialização reversível, valores dentro de limites ou equivalência entre implementações — e adicione geradores focados no domínio.
Se o time já possui uma boa suíte JUnit 5, preserve o que funciona. Kotest vale a pena quando reduz ruído, melhora o diagnóstico ou encontra combinações que testes manuais deixariam passar. Para montar a estratégia completa, conecte este guia ao conteúdo de testes em Kotlin, aos testes com JUnit 5 e MockK e aos testes de integração com Testcontainers.