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.
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.
Configurando o projeto
Adicione as dependências no seu build.gradle.kts:
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("io.mockk:mockk:1.13.10")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
}
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.
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", "maria@email.com")
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", "joao@email.com")
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", "carlos@email.com")
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.
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.
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.
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 e use @Nested para organizar seus testes de forma hierárquica.
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.