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 classes final (o padrão em Kotlin) e coroutines de forma nativa. Combine os dois com kotlinx-coroutines-test (runTest) para cobrir código assíncrono. Instale com testImplementation, habilite useJUnitPlatform() e use every { } returns para mocks síncronos e coEvery { } returns para suspend.

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érioMockKMockito + mockito-kotlin
Classes final (padrão em Kotlin)Suporte nativoPrecisa de mockito-inline ou config
Coroutines (suspend)coEvery/coVerify nativosExige mockito-coroutines ou workaround
DSL idiomática KotlinSim (every { } returns)Parcial (extensões)
relaxed por padrãoSimNão
Spy de objetos reaisspyk()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.