Kotlin: Testes Kotlin com JUnit 5 e MockK Explicados

Última atualização: 2026-08-26

O lema de Bob: código não testado são suposições não verificadas. JUnit 5 + MockK é a combinação dourada de testes Kotlin — MockK é projetado nativamente para Kotlin (suportando mocking de corrotinas), e JUnit 5 fornece um framework de testes moderno.

1. O que Você Aprenderá


2. A História Real de um Líder de QA

(1) Problema: Incompatibilidade do Mockito com Kotlin

Bob frequentemente enfrentava erros de classe Final ao mockar classes Kotlin com Mockito (classes Kotlin são final por padrão), exigindo configuração da extensão mock-maker-inline. E funções suspend eram completamente impossíveis de mockar.

(2) Solução do MockK

KOTLIN
// Mockito: problemas com classes finais e funções suspend
when(repo.findById("ORD-001")).thenReturn(order)  // Falha em classe final!

// MockK: nativo para Kotlin, suporta tudo
every { repo.findById("ORD-001") } returns order  // Funciona em qualquer classe!
coEvery { repo.fetchOrderAsync("ORD-001") } returns order  // Funções suspend!

MockK é projetado nativamente para Kotlin — sem configuração extra necessária, com suporte completo para classes finais, funções de extensão e corrotinas.


3. Noções Básicas do JUnit 5

(1) Configuração de Dependências

KOTLIN
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
    testImplementation("io.mockk:mockk:1.13.8")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}

tasks.test {
    useJUnitPlatform()
}

(2) Testes Básicos

KOTLIN
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*

class OrderTest {
    @Test
    fun `should create order with correct id`() {
        val order = Order("ORD-001", 299.99, "PENDING")
        assertEquals("ORD-001", order.id)
        assertEquals(299.99, order.total)
        assertEquals("PENDING", order.status)
    }

    @Test
    fun `should throw on negative total`() {
        assertThrows(IllegalArgumentException::class.java) {
            Order("ORD-001", -50.0, "PENDING")
        }
    }
}

(3) Testes Parametrizados

KOTLIN
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource

class OrderPriorityTest {
    @ParameterizedTest
    @CsvSource(
        "100.0, LOW",
        "1500.0, MEDIUM",
        "15000.0, HIGH"
    )
    fun `should determine priority based on total`(total: Double, expected: String) {
        val priority = when {
            total > 10_000 -> "HIGH"
            total > 1_000 -> "MEDIUM"
            else -> "LOW"
        }
        assertEquals(expected, priority)
    }
}

(4) Testes Aninhados

KOTLIN
class OrderProcessorTest {
    @Nested
    inner class OrderCreation {
        @Test
        fun `should create pending order`() { ... }

        @Test
        fun `should reject negative total`() { ... }
    }

    @Nested
    inner class OrderProcessing {
        @Test
        fun `should confirm pending order`() { ... }

        @Test
        fun `should reject confirming shipped order`() { ... }
    }
}

4. Noções Básicas do MockK

(1) Criação de Mocks e Stubbing

KOTLIN
import io.mockk.*

class OrderServiceTest {
    // Criar mock
    val repo = mockk<OrderRepository>()
    val service = OrderService(repo)

    @Test
    fun `should find order by id`() {
        // Stub: definir comportamento
        every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "PENDING")
        every { repo.findById(any()) } returns null

        // Agir
        val order = service.findOrder("ORD-001")

        // Verificar
        assertEquals(299.99, order?.total)

        // Verificar: checar interação
        verify(exactly = 1) { repo.findById("ORD-001") }
    }
}

(2) API Comum do MockK

API Propósito Exemplo
every { } returns Stub valor de retorno every { repo.findById(any()) } returns order
every { } throws Stub exceção every { repo.save(any()) } throws SQLException()
every { } answers Cálculo dinâmico every { repo.findById(any()) } answers { ... }
verify { } Verificar se a chamada ocorreu verify { repo.save(order) }
verifySequence { } Verificar ordem das chamadas verifySequence { f1(); f2() }
confirmVerified(repo) Confirmar sem chamadas não verificadas confirmVerified(repo)
coEvery { } returns Stub de corrotina coEvery { repo.fetchAsync(any()) } returns order
coVerify { } Verificação de corrotina coVerify { repo.fetchAsync(id) }

(3) MockK vs Mockito

Dimensão Mockito MockK
Classes finais Kotlin Requer configuração extra Suporte nativo
Mocking de corrotinas Não suportado coEvery / coVerify
Mocking de funções de extensão Não suportado mockkStatic
Mocking de objetos Difícil mockkObject
Sintaxe Estilo Java Estilo DSL Kotlin

5. Testes de Corrotinas

(1) runTest

KOTLIN
import kotlinx.coroutines.test.runTest

class OrderServiceTest {
    val repo = mockk<OrderRepository>()
    val service = OrderService(repo)

    @Test
    fun `should fetch order asynchronously`() = runTest {
        // Dado
        coEvery { repo.fetchOrder("ORD-001") } returns Order("ORD-001", 299.99, "PENDING")

        // Quando
        val order = service.fetchOrderAsync("ORD-001")

        // Então
        assertEquals(299.99, order.total)
        coVerify { repo.fetchOrder("ORD-001") }
    }
}

(2) Ferramentas de Teste de Corrotinas

Ferramenta Propósito
runTest Ponto de entrada para teste de corrotinas (substitui runBlocking)
coEvery Stub funções de corrotina
coVerify Verificar chamadas de funções de corrotina
TestDispatcher Controlar despacho de corrotinas (tempo virtual)
advanceUntilIdle() Avançar todas as corrotinas pendentes

6. Ciclo de Vida de Testes

KOTLIN
class OrderServiceTest {
    private lateinit var repo: OrderRepository
    private lateinit var service: OrderService

    @BeforeEach
    fun setup() {
        repo = mockk()
        service = OrderService(repo)
    }

    @AfterEach
    fun cleanup() {
        unmockkAll()  // Limpar todos os mocks
    }

    @Test
    fun `should process order`() { ... }
}

(1) Anotações de Ciclo de Vida

Anotação Momento de Execução Propósito
@BeforeEach Antes de cada teste Inicializar mocks e objetos de teste
@AfterEach Depois de cada teste Limpar recursos
@BeforeAll Antes de todos os testes (precisa de companion object) Inicialização custosa única
@AfterAll Depois de todos os testes Limpeza global

7. Pirâmide de Testes e Fluxo

100%
flowchart TD
    A[Testes Unitários<br/>MockK + JUnit5<br/>Rápidos, isolados] --> B[Testes de Integração<br/>Spring Boot Test<br/>DB/HTTP real]
    B --> C[Testes E2E<br/>TestContainers<br/>Stack completa]
    A --> D[runTest para corrotinas]
    A --> E[MockK para dependências]
    D --> F[Controle de tempo virtual]

8. Exemplo Completo: OrderProcessorTest

KOTLIN
// ============================================
// OrderProcessor - Suíte de Testes
// Funcionalidade: Cobertura completa de testes com JUnit5 + MockK
// ============================================

import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
import io.mockk.*
import kotlinx.coroutines.test.runTest

// Classes de domínio
data class Order(val id: String, val total: Double, var status: String, val customerId: String)

interface OrderRepository {
    fun findById(id: String): Order?
    fun save(order: Order)
    suspend fun fetchOrderAsync(id: String): Order
}

class OrderService(private val repo: OrderRepository) {
    fun findOrder(id: String): Order = repo.findById(id) ?: throw NoSuchElementException("Order $id not found")

    fun processOrder(id: String): Order {
        val order = findOrder(id)
        if (order.status != "PENDING") throw IllegalStateException("Order $id is not pending")
        order.status = "CONFIRMED"
        repo.save(order)
        return order
    }

    suspend fun fetchOrderAsync(id: String): Order = repo.fetchOrderAsync(id)

    fun calculatePriority(order: Order): String = when {
        order.total > 10_000 -> "HIGH"
        order.total > 1_000 -> "MEDIUM"
        else -> "LOW"
    }
}

class OrderServiceTest {
    private lateinit var repo: OrderRepository
    private lateinit var service: OrderService

    @BeforeEach
    fun setup() {
        repo = mockk(relaxed = true)  // Relaxed: retorna valores padrão para métodos sem stub
        service = OrderService(repo)
    }

    @AfterEach
    fun cleanup() {
        unmockkAll()
    }

    @Nested
    @DisplayName("Order Retrieval")
    inner class OrderRetrieval {
        @Test
        fun `should find existing order`() {
            // Dado
            every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "PENDING", "CUST-001")

            // Quando
            val order = service.findOrder("ORD-001")

            // Então
            assertEquals("ORD-001", order.id)
            assertEquals(299.99, order.total)
        }

        @Test
        fun `should throw when order not found`() {
            every { repo.findById(any()) } returns null
            assertThrows(NoSuchElementException::class.java) {
                service.findOrder("ORD-999")
            }
        }
    }

    @Nested
    @DisplayName("Order Processing")
    inner class OrderProcessing {
        @Test
        fun `should confirm pending order`() {
            every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "PENDING", "CUST-001")
            every { repo.save(any()) } just Runs

            val processed = service.processOrder("ORD-001")

            assertEquals("CONFIRMED", processed.status)
            verify { repo.save(match { it.status == "CONFIRMED" }) }
        }

        @Test
        fun `should reject non-pending order`() {
            every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "SHIPPED", "CUST-001")

            assertThrows(IllegalStateException::class.java) {
                service.processOrder("ORD-001")
            }
            verify(exactly = 0) { repo.save(any()) }
        }
    }

    @Nested
    @DisplayName("Priority Calculation")
    inner class PriorityCalculation {
        @Test
        fun `should assign HIGH priority for orders over 10000`() {
            val order = Order("ORD-001", 15_000.00, "PENDING", "CUST-001")
            assertEquals("HIGH", service.calculatePriority(order))
        }

        @Test
        fun `should assign MEDIUM priority for orders between 1000 and 10000`() {
            val order = Order("ORD-001", 2_500.00, "PENDING", "CUST-001")
            assertEquals("MEDIUM", service.calculatePriority(order))
        }

        @Test
        fun `should assign LOW priority for orders under 1000`() {
            val order = Order("ORD-001", 299.99, "PENDING", "CUST-001")
            assertEquals("LOW", service.calculatePriority(order))
        }
    }

    @Nested
    @DisplayName("Async Operations")
    inner class AsyncOperations {
        @Test
        fun `should fetch order asynchronously`() = runTest {
            coEvery { repo.fetchOrderAsync("ORD-001") } returns Order("ORD-001", 299.99, "PENDING", "CUST-001")

            val order = service.fetchOrderAsync("ORD-001")

            assertEquals("ORD-001", order.id)
            coVerify { repo.fetchOrderAsync("ORD-001") }
        }
    }
}

Saída: Todos os testes passam ✅


9. Exemplos práticos rápidos

▶ Exemplo: Teste JUnit 5 básico

KOTLIN
import org.junit.jupiter.api.*

class CalculatorTest {

    @Test
    fun `soma dois números positivos`() {
        val result = 2 + 3
        Assertions.assertEquals(5, result)
    }

    @Test
    fun `lista contém elemento`() {
        val list = listOf("a", "b", "c")
        Assertions.assertTrue("b" in list)
    }

    @BeforeEach
    fun setUp() {
        println("Setup executado antes de cada teste")
    }

    @AfterEach
    fun tearDown() {
        println("Cleanup executado após cada teste")
    }

    @DisplayName("Demonstração de múltiplos asserts")
    @Test
    fun multipleAssertions() {
        val order = mapOf("id" to "ORD-001", "total" to 299.99)
        Assertions.assertAll(
            { Assertions.assertEquals("ORD-001", order["id"]) },
            { Assertions.assertEquals(299.99, order["total"]) }
        )
    }
}

▶ Exemplo: MockK - mock e verify

KOTLIN
import io.mockk.*

class OrderService(val paymentGateway: PaymentGateway) {
    fun processOrder(id: String, amount: Double) {
        paymentGateway.charge(id, amount)
    }
}

interface PaymentGateway {
    fun charge(orderId: String, amount: Double): Boolean
}

class OrderServiceTest {
    private val gateway = mockk<PaymentGateway>()
    private val service = OrderService(gateway)

    @Test
    fun `processOrder chama gateway corretamente`() {
        every { gateway.charge("ORD-001", 299.99) } returns true

        service.processOrder("ORD-001", 299.99)

        verify { gateway.charge("ORD-001", 299.99) }
        verify(exactly = 1) { gateway.charge(any(), any()) }
    }
}

▶ Exemplo: Argument matchers em MockK

KOTLIN
import io.mockk.*

class Notifier {
    fun send(message: String): Boolean = true
}

class NotificationServiceTest {
    private val notifier = mockk<Notifier>()

    @Test
    fun `captura argumentos com any e slot`() {
        val slot = slot<String>()
        every { notifier.send(capture(slot)) } returns true

        notifier.send("First message")
        notifier.send("Second message")

        // Assertions sobre argumentos capturados
        println("Captured messages: ${slot.all}")
        verify(exactly = 2) { notifier.send(any()) }
    }
}

▶ Exemplo: Testes parametrizados com @ParameterizedTest

KOTLIN
import org.junit.jupiter.params.*
import org.junit.jupiter.params.provider.*

class ParameterizedTest {

    @ParameterizedTest
    @ValueSource(ints = [1, 2, 3, 4, 5])
    fun `números positivos são verdadeiros`(number: Int) {
        Assertions.assertTrue(number > 0)
    }

    @ParameterizedTest
    @CsvSource(
        "1, 1, 2",
        "2, 3, 5",
        "10, 20, 30"
    )
    fun `soma de dois inteiros`(a: Int, b: Int, expected: Int) {
        Assertions.assertEquals(expected, a + b)
    }

    @ParameterizedTest
    @MethodSource("validOrderIds")
    fun `todos IDs são válidos`(id: String) {
        Assertions.assertTrue(id.startsWith("ORD-"))
    }

    companion object {
        @JvmStatic
        fun validOrderIds() = listOf("ORD-001", "ORD-002", "ORD-999")
    }
}

▶ Exemplo: Testes de exception

KOTLIN
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.assertThrows

class ExceptionTest {

    @Test
    fun `lança IllegalArgumentException para entrada inválida`() {
        assertThrows<IllegalArgumentException> {
            require(false) { "Mensagem de erro" }
        }
    }

    @Test
    fun `mensagem da exceção`() {
        val exception = assertThrows<NumberFormatException> {
            "não é um número".toInt()
        }
        Assertions.assertTrue(exception.message!!.contains("não"))
    }
}

▶ Exemplo: Spy com MockK (verificação parcial)

KOTLIN
import io.mockk.*

class Service {
    fun calculate(input: Int): Int = input * 2
    fun log(msg: String) = println("LOG: $msg")
}

class SpyTest {
    @Test
    fun `spy chama método real`() {
        val service = spyk(Service())

        // Stub parcial - apenas intercepta calculate(5)
        every { service.calculate(5) } returns 999

        // Outras chamadas usam implementação real
        println("calculate(5) = ${service.calculate(5)}")  // 999
        println("calculate(10) = ${service.calculate(10)}") // 20 (real)

        verify { service.calculate(5) }
        verify { service.calculate(10) }
    }
}

▶ Exemplo: TestCoroutineRule para coroutines

KOTLIN
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*

class CoroutineTest {

    @Test
    fun `testAsync operation with runTest`() = runTest {
        val result = async {
            delay(100)
            "loaded"
        }.await()

        Assertions.assertEquals("loaded", result)
        println("Result: $result")
    }

    @Test
    fun `verifica tempo virtual`() = runTest {
        val startTime = currentTime

        delay(1000)

        val elapsed = currentTime - startTime
        Assertions.assertEquals(1000L, elapsed)
        println("Elapsed virtual: ${elapsed}ms")
    }
}

❓ Perguntas Frequentes

P: Qual a diferença entre mock relaxed do MockK e mock regular? R: Um mock relaxed (mockk(relaxed = true)) retorna valores padrão (0/null/false) para métodos sem stub; um mock regular lança MockKException para métodos sem stub. Prefira mocks regulares para evitar esconder comportamentos não configurados.

P: Qual a diferença entre runTest e runBlocking? R: runTest usa tempo virtual (pula delay), então os testes completam em milissegundos; runBlocking usa tempo real e espera pela duração real do delay. Testes de corrotinas devem usar runTest.

P: Como mockar um companion object? R: Use mockkObject(Order) para mockar o companion object inteiro, depois every { ... } returns. Restaure com unmockkObject(Order) após o teste.

P: Como mockar funções de extensão? R: Use mockkStatic("package.ClassNameKt") para mockar funções de extensão específicas. No entanto, tente evitar — funções de extensão frequentemente podem ser refatoradas em funções regulares para melhor testabilidade.

P: Qual porcentagem de cobertura de testes é suficiente? R: Meta de 80% de cobertura de linhas, mas cobertura não equivale a qualidade. Lógica de negócio crítica (pagamentos, estoque, máquinas de estado) deve ter 100% de cobertura; data classes simples podem permanecer sem teste.

P: Como distinguir testes de integração de testes unitários? R: Testes unitários mockam todas as dependências (testando uma única classe); testes de integração usam dependências reais (banco de dados, HTTP). Testes unitários são rápidos e estáveis; testes de integração são mais lentos mas mais realistas.


📖 Resumo


📝 Exercícios

  1. Iniciante (⭐): Escreva testes JUnit 5 para Order para verificar o comportamento de id e status. Dica: @Test fun \should create order`() { ... }`
  2. Intermediário (⭐⭐): Use MockK para mockar OrderRepository e testar OrderService.processOrder() tanto no caminho feliz quanto no de erro. Dica: every { repo.findById(any()) } returns order
  3. Desafio (⭐⭐⭐): Use runTest + coEvery para testar uma função assíncrona de busca de pedidos, verificando o timing das chamadas de corrotinas. Dica: coEvery { repo.fetchOrderAsync(any()) } returns ...

← Anterior | Próximo →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%