Testar código em Kotlin é uma experiência completamente diferente de testar em Java. A linguagem oferece recursos como data classes, extension functions, coroutines e null safety que mudam a forma como escrevemos — e testamos — nosso código. Neste guia prático, vamos explorar como usar JUnit 5 e MockK para escrever testes unitários idiomáticos em Kotlin, do primeiro @Test até mocks de coroutines com coEvery.
Resumo rápido (TL;DR): JUnit 5 (
junit-jupiter) é o framework de execução padrão em 2026; MockK (io.mockk:mockk) substitui o Mockito porque entende classesfinal(o padrão em Kotlin) e coroutines de forma nativa. Combine os dois comkotlinx-coroutines-test(runTest) para cobrir código assíncrono. Instale comtestImplementation, habiliteuseJUnitPlatform()e useevery { } returnspara mocks síncronos ecoEvery { } returnsparasuspend.
Se você prefere uma visão estratégica antes de mergulhar no código — pirâmide de testes, quando usar unitários vs. integração vs. E2E, Ktor, Spring Boot e Android —, comece pelo nosso guia completo de testes em Kotlin. Este artigo é o companheiro prático e aprofundado de JUnit 5 + MockK.
Por que JUnit 5 + MockK?
No ecossistema Java, a combinação clássica é JUnit + Mockito. Funciona em Kotlin? Sim, mas com ressalvas. Mockito tem dificuldades com classes final (que são o padrão em Kotlin), exige workarounds com mockito-kotlin e não suporta nativamente coroutines.
MockK foi criado especificamente para Kotlin. Ele entende classes finais, oferece uma DSL fluente e idiomática, e suporta coroutines, relaxed mocks e muito mais — tudo de forma nativa.
Já o JUnit 5 (Jupiter) trouxe melhorias significativas sobre o JUnit 4: testes parametrizados mais poderosos, extensões modulares, lifecycle hooks e suporte a Kotlin muito mais robusto.
MockK vs. Mockito em Kotlin
| Critério | MockK | Mockito + mockito-kotlin |
|---|---|---|
Classes final (padrão em Kotlin) | Suporte nativo | Precisa de mockito-inline ou config |
Coroutines (suspend) | coEvery/coVerify nativos | Exige mockito-coroutines ou workaround |
| DSL idiomática Kotlin | Sim (every { } returns) | Parcial (extensões) |
relaxed por padrão | Sim | Não |
| Spy de objetos reais | spyk() | spy() |
Para projetos Kotlin puros em 2026, MockK é a escolha com menos fricção. Para times mistos Java/Kotlin que já usam Mockito, a migração pode ser gradual.
Configurando o projeto
Adicione as dependências no seu build.gradle.kts (versões vigentes em 2026):
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")
testImplementation("io.mockk:mockk:1.13.13")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
}
tasks.test {
useJUnitPlatform()
}
Com essas três dependências, você tem tudo o que precisa: JUnit 5 como framework de teste, MockK para mocking e a biblioteca de testes de coroutines. Verifique sempre a versão mais recente no Maven Central, pois patches de segurança são publicados com frequência.
Dica de pipeline: conecte essa suíte de testes ao seu pipeline de CI/CD em Kotlin e complemente com detekt e ktlint para garantir qualidade de código e comportamento a cada commit.
Escrevendo seu primeiro teste
Vamos começar com um exemplo simples. Imagine um serviço que calcula descontos:
class DescontoService {
fun calcularDesconto(preco: Double, percentual: Int): Double {
require(percentual in 0..100) { "Percentual deve estar entre 0 e 100" }
return preco * (1 - percentual / 100.0)
}
}
O teste com JUnit 5 fica assim:
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
class DescontoServiceTest {
private val service = DescontoService()
@Test
fun `deve aplicar desconto de 20 por cento`() {
val resultado = service.calcularDesconto(100.0, 20)
assertEquals(80.0, resultado, 0.01)
}
@Test
fun `deve lancar excecao para percentual invalido`() {
assertThrows<IllegalArgumentException> {
service.calcularDesconto(100.0, 150)
}
}
}
Note o uso de backticks nos nomes dos testes — um recurso do Kotlin que permite nomes descritivos e legíveis. Esse é um dos grandes diferenciais ao testar em Kotlin versus Java.
Mocking com MockK — O básico
O poder do MockK aparece quando você precisa isolar dependências. Vamos considerar um repositório e um serviço:
interface UsuarioRepository {
fun buscarPorId(id: Long): Usuario?
fun salvar(usuario: Usuario): Usuario
}
data class Usuario(val id: Long, val nome: String, val email: String)
class UsuarioService(private val repository: UsuarioRepository) {
fun obterUsuario(id: Long): Usuario {
return repository.buscarPorId(id)
?: throw NoSuchElementException("Usuário não encontrado")
}
}
Com MockK, o teste fica extremamente limpo:
import io.mockk.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
class UsuarioServiceTest {
private val repository = mockk<UsuarioRepository>()
private val service = UsuarioService(repository)
@Test
fun `deve retornar usuario quando encontrado`() {
val usuario = Usuario(1L, "Maria", "[email protected]")
every { repository.buscarPorId(1L) } returns usuario
val resultado = service.obterUsuario(1L)
assertEquals("Maria", resultado.nome)
verify(exactly = 1) { repository.buscarPorId(1L) }
}
@Test
fun `deve lancar excecao quando usuario nao encontrado`() {
every { repository.buscarPorId(any()) } returns null
assertThrows<NoSuchElementException> {
service.obterUsuario(99L)
}
}
}
A sintaxe every { ... } returns ... é a DSL do MockK. É declarativa, fácil de ler e não exige casts estranhos como acontece com Mockito em Kotlin.
Capturando argumentos com Slot
Às vezes, você precisa inspecionar o que foi passado para um mock. O slot do MockK resolve isso:
@Test
fun `deve salvar usuario com nome formatado`() {
val slot = slot<Usuario>()
every { repository.salvar(capture(slot)) } answers { slot.captured }
val service = UsuarioService(repository)
service.criarUsuario("joão silva", "[email protected]")
assertEquals("João Silva", slot.captured.nome)
}
Isso é especialmente útil quando seu serviço transforma dados antes de salvar — você captura exatamente o que foi enviado ao repositório.
Testando coroutines com coEvery
Kotlin brilha em código assíncrono com coroutines, e o MockK suporta isso nativamente com coEvery e coVerify:
interface NotificacaoService {
suspend fun enviar(usuarioId: Long, mensagem: String): Boolean
}
class PedidoService(
private val repository: UsuarioRepository,
private val notificacao: NotificacaoService
) {
suspend fun processarPedido(usuarioId: Long) {
val usuario = repository.buscarPorId(usuarioId)
?: throw NoSuchElementException("Usuário não encontrado")
notificacao.enviar(usuarioId, "Pedido processado, ${usuario.nome}!")
}
}
O teste usa runTest da biblioteca kotlinx-coroutines-test:
import kotlinx.coroutines.test.runTest
import io.mockk.*
import org.junit.jupiter.api.Test
class PedidoServiceTest {
private val repository = mockk<UsuarioRepository>()
private val notificacao = mockk<NotificacaoService>()
private val service = PedidoService(repository, notificacao)
@Test
fun `deve enviar notificacao ao processar pedido`() = runTest {
val usuario = Usuario(1L, "Carlos", "[email protected]")
every { repository.buscarPorId(1L) } returns usuario
coEvery { notificacao.enviar(any(), any()) } returns true
service.processarPedido(1L)
coVerify { notificacao.enviar(1L, "Pedido processado, Carlos!") }
}
}
O coEvery e coVerify são as versões suspending dos every e verify tradicionais. Sem eles, testar código com coroutines seria muito mais trabalhoso.
E os testes de Flow com Turbine?
Quando o seu código expõe Flow, StateFlow ou SharedFlow, o JUnit 5 + MockK continua sendo a base, mas você adiciona a Turbine para assertar emissões ao longo do tempo. Por exemplo, awaitItem(), awaitComplete() e tratamento de erros virtuais com runTest. Para um aprofundamento completo — incluindo TestDispatcher, virtual time e armadilhas comuns —, veja nosso guia de testes de Flow, StateFlow e Turbine.
Relaxed mocks e spy
Nem sempre você quer configurar cada chamada de um mock. O relaxed mock retorna valores padrão para qualquer chamada não configurada:
val repository = mockk<UsuarioRepository>(relaxed = true)
// repository.buscarPorId(1L) retorna null (valor padrão para tipo nullable)
// repository.salvar(usuario) retorna um Usuario com valores padrão
Já o spy permite mockar parcialmente um objeto real:
val service = spyk(UsuarioService(repository))
every { service.validarEmail(any()) } returns true
// Outras funções continuam com a implementação real
Boas práticas para testes em Kotlin
1. Use nomes descritivos com backticks
@Test
fun `deve calcular frete grátis para compras acima de R$200`() { }
2. Organize com @Nested
O JUnit 5 permite agrupar testes relacionados com @Nested:
class CarrinhoServiceTest {
@Nested
inner class `Ao adicionar item` {
@Test
fun `deve atualizar o total`() { }
@Test
fun `deve incrementar a quantidade`() { }
}
@Nested
inner class `Ao remover item` {
@Test
fun `deve recalcular o total`() { }
}
}
3. Use testes parametrizados
@ParameterizedTest
@CsvSource("100.0, 10, 90.0", "200.0, 50, 100.0", "50.0, 0, 50.0")
fun `deve calcular desconto corretamente`(preco: Double, percentual: Int, esperado: Double) {
assertEquals(esperado, service.calcularDesconto(preco, percentual), 0.01)
}
4. Limpe os mocks entre testes
@AfterEach
fun tearDown() {
clearAllMocks()
}
5. Prefira injeção de dependência
Classes que recebem dependências via construtor são infinitamente mais fáceis de testar. Se você precisa de mockk em tudo, é sinal de boa arquitetura. Se precisa de reflection ou hacks, algo pode ser melhorado no design.
Kotest: quando a DSL do JUnit 5 não é suficiente
Para times que querem uma sintaxe ainda mais expressiva, o Kotest oferece estilos como StringSpec, BehaviorSpec e FreeSpec, além de matchers fluídos (value shouldBe 80.0) e inspectores (forall). Kotest não substitui JUnit 5 — ele roda por cima dele (ou com seu próprio engine) e integra-se bem ao MockK. Vale a pena quando a legibilidade dos testes de domínio é prioridade. O guia de estratégia de testes discute em detalhe quando adotar Kotest no lugar do JUnit 5 puro.
Testes no Android
No Android, JUnit 5 + MockK cobrem a camada de domínio e ViewModel, mas a interface com Jetpack Compose pede ferramentas específicas: createAndroidComposeRule, snapshot tests e frameworks de UI automation como o Maestro. Se você testa ViewModels com coroutines, o mesmo padrão runTest + coEvery mostrado acima se aplica diretamente. Para a camada de UI e testes instrumentados, consulte o guia de testes Android com Compose e Maestro.
Testes vs. design patterns
Se você está usando design patterns em Kotlin, como Strategy ou Observer, os testes ficam naturalmente mais simples. Padrões que favorecem composição sobre herança se alinham perfeitamente com a filosofia de MockK.
Para projetos que usam programação funcional com Arrow, os testes também mudam de abordagem — com Either e Raise, você testa caminhos de sucesso e erro sem exceções, o que torna os testes mais previsíveis.
Perguntas frequentes (FAQ)
JUnit 5 ou JUnit 4 em projetos Kotlin novos?
JUnit 5. O JUnit 4 está em modo de manutenção; o 5 traz testes parametrizados mais poderosos, @Nested, extensões e melhor suporte a Kotlin. A migração é direta e a comunidade já adota o 5 como padrão em 2026.
MockK ou Mockito em Kotlin?
MockK, para projetos Kotlin. Ele lida com classes final e coroutines nativamente, sem a configuração extra que o Mockito exige. Mockito continua válido em codebases Java legadas compartilhadas.
Como testar funções suspend?
Use runTest do kotlinx-coroutines-test para um dispatcher virtual controlado, e coEvery { } returns / coVerify { } do MockK para mocks suspensos. Evite runBlocking em testes unitários — ele usa threads reais e torna a suíte mais lenta.
MockK funciona com coroutines e Flow?
Sim. Para suspend, use coEvery/coVerify. Para Flow, combine MockK com a Turbine (awaitItem()) para assertar emissões. Veja o guia de testes de Flow com Turbine.
Preciso de clearAllMocks() em todo teste?
Depende. Se você cria mocks frescos por teste (instância de mockk no campo da classe recriada a cada teste), não precisa. Em suítes grandes com mocks compartilhados, @AfterEach { clearAllMocks() } evita vazamento de estado entre testes.
JUnit 5 roda no Android?
Sim, para testes unitários da JVM (test/). Para testes instrumentados (androidTest/), o ecossistema ainda usa mais JUnit 4 + AndroidX Test, embora existam extensões para JUnit 5. Testes de ViewModel com coroutines rodam tranquilamente com JUnit 5 + runTest.
Conclusão
A combinação de JUnit 5 + MockK é, sem dúvida, o padrão ouro para testes em Kotlin em 2026. MockK entende a linguagem nativamente, JUnit 5 oferece uma estrutura moderna e extensível, e juntos eles permitem escrever testes que são tão idiomáticos quanto o código de produção.
Se você ainda está usando Mockito com Kotlin, considere experimentar MockK — a curva de aprendizado é baixa e os benefícios são imediatos. Comece pelos testes mais simples, explore coEvery para código assíncrono, adicione Turbine para Flow e use @Nested para organizar seus testes de forma hierárquica. Para montar a estratégia completa de testes (unitários, integração com Testcontainers, Ktor, Spring Boot e Android), o ponto de partida é o guia completo de testes em Kotlin.
Se você trabalha com múltiplas linguagens, vale comparar abordagens: em Python, pytest oferece fixtures e mocking com uma filosofia mais dinâmica, enquanto em Go, o pacote testing padrão favorece testes sem mocks com interfaces implícitas. Já em Rust, testes são integrados ao compilador com cargo test, garantindo segurança de memória até nos testes.
Bons testes não são um custo — são um investimento na qualidade e confiança do seu projeto.