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á
- JUnit 5 + Kotlin: testes
@Test,@ParameterizedTest,@Nested - MockK: framework de mocking nativo para Kotlin
- Testes de corrotinas:
runTest/coEvery/coVerify - Ciclo de vida de testes:
@BeforeEach/@AfterEach - Charlie em ação: cobertura completa do OrderProcessorTest
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
// 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
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
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
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
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
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
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
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
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
// ============================================
// 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
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
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
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
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
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)
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
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:
runTestusa tempo virtual (puladelay), então os testes completam em milissegundos;runBlockingusa tempo real e espera pela duração real do delay. Testes de corrotinas devem usarrunTest.
P: Como mockar um companion object? R: Use
mockkObject(Order)para mockar o companion object inteiro, depoisevery { ... } returns. Restaure comunmockkObject(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
- JUnit 5 + MockK é a combinação dourada de testes Kotlin
- MockK suporta nativamente Kotlin: classes finais, corrotinas, funções de extensão, objetos
every { } returnspara stubbing,verify { }para verificar chamadasrunTestpara testes de corrotinas, usando tempo virtual sem necessidade de esperar pordelaycoEvery/coVerifyespecificamente para mocking e verificação de funções suspend- Testes aninhados (
@Nested) organizam testes relacionados; testes parametrizados (@ParameterizedTest) reduzem duplicação
📝 Exercícios
- Iniciante (⭐): Escreva testes JUnit 5 para
Orderpara verificar o comportamento deidestatus. Dica:@Test fun \should create order`() { ... }` - Intermediário (⭐⭐): Use MockK para mockar
OrderRepositorye testarOrderService.processOrder()tanto no caminho feliz quanto no de erro. Dica:every { repo.findById(any()) } returns order - Desafio (⭐⭐⭐): Use
runTest+coEverypara testar uma função assíncrona de busca de pedidos, verificando o timing das chamadas de corrotinas. Dica:coEvery { repo.fetchOrderAsync(any()) } returns ...