Testes automatizados sao fundamentais para garantir a qualidade e a confiabilidade de qualquer aplicacao. Em Kotlin, o ecossistema de testes e rico e produtivo, combinando frameworks maduros como JUnit 5 com bibliotecas idiomaticas como MockK e Kotest. A expressividade do Kotlin torna os testes mais legiveise concisos do que em Java, com recursos como extension functions, data classes e DSLs que facilitam a criacao de cenarios de teste claros e manuteníveis. Neste guia, cobrimos testes unitarios, de integracao e as melhores praticas para manter sua suite de testes saudavel.

Configurando o Ambiente de Testes

Adicione as dependencias de teste ao build.gradle.kts:

dependencies {
    // JUnit 5
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // MockK para mocks
    testImplementation("io.mockk:mockk:1.13.9")

    // Assertions avancadas
    testImplementation("org.assertj:assertj-core:3.25.1")

    // Coroutines test
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")

    // Kotest (opcional, alternativa ao JUnit)
    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
    testImplementation("io.kotest:kotest-assertions-core:5.8.0")
}

tasks.test {
    useJUnitPlatform()
}

Testes Unitarios com JUnit 5

Os testes unitarios verificam unidades isoladas de codigo. Com Kotlin, a sintaxe e limpa e direta:

class CalculadoraDeDescontoTest {

    private lateinit var calculadora: CalculadoraDeDesconto

    @BeforeEach
    fun setup() {
        calculadora = CalculadoraDeDesconto()
    }

    @Test
    fun `deve aplicar desconto percentual corretamente`() {
        val resultado = calculadora.aplicarDesconto(
            valorOriginal = 100.0,
            percentualDesconto = 15.0
        )

        assertEquals(85.0, resultado, 0.01)
    }

    @Test
    fun `deve lancar excecao para desconto negativo`() {
        assertThrows<IllegalArgumentException> {
            calculadora.aplicarDesconto(100.0, -5.0)
        }
    }

    @Test
    fun `deve retornar zero para desconto de 100 porcento`() {
        val resultado = calculadora.aplicarDesconto(250.0, 100.0)
        assertEquals(0.0, resultado, 0.01)
    }

    @ParameterizedTest
    @CsvSource(
        "100.0, 10.0, 90.0",
        "200.0, 25.0, 150.0",
        "50.0, 50.0, 25.0"
    )
    fun `deve calcular desconto para varios cenarios`(
        valor: Double,
        desconto: Double,
        esperado: Double
    ) {
        val resultado = calculadora.aplicarDesconto(valor, desconto)
        assertEquals(esperado, resultado, 0.01)
    }
}

O Kotlin permite nomes de teste descritivos com backticks, tornando o relatorio de testes muito mais legivel.

Mocking com MockK

MockK e a biblioteca de mocking preferida para Kotlin, oferecendo suporte nativo a coroutines, extension functions e DSL idiomatica:

class ProdutoServiceTest {

    @MockK
    private lateinit var repository: ProdutoRepository

    @MockK
    private lateinit var notificador: NotificadorDePreco

    private lateinit var service: ProdutoService

    @BeforeEach
    fun setup() {
        MockKAnnotations.init(this)
        service = ProdutoService(repository, notificador)
    }

    @Test
    fun `deve retornar produto quando encontrado`() {
        // Arrange
        val produto = Produto(id = 1L, nome = "Teclado", preco = 150.0)
        every { repository.buscarPorId(1L) } returns produto

        // Act
        val resultado = service.buscarPorId(1L)

        // Assert
        assertEquals("Teclado", resultado.nome)
        verify(exactly = 1) { repository.buscarPorId(1L) }
    }

    @Test
    fun `deve lancar excecao quando produto nao encontrado`() {
        every { repository.buscarPorId(any()) } returns null

        assertThrows<RecursoNaoEncontradoException> {
            service.buscarPorId(999L)
        }
    }

    @Test
    fun `deve notificar quando preco e alterado`() {
        val produto = Produto(id = 1L, nome = "Teclado", preco = 150.0)
        every { repository.buscarPorId(1L) } returns produto
        every { repository.salvar(any()) } returns produto.copy(preco = 120.0)
        every { notificador.notificarMudancaPreco(any(), any(), any()) } just Runs

        service.atualizarPreco(1L, 120.0)

        verify {
            notificador.notificarMudancaPreco(
                produtoId = 1L,
                precoAntigo = 150.0,
                precoNovo = 120.0
            )
        }
    }

    @AfterEach
    fun tearDown() {
        unmockkAll()
    }
}

Mocking de Coroutines

class ProdutoServiceCoroutineTest {

    @MockK
    private lateinit var apiClient: ApiClient

    private lateinit var service: ProdutoService

    @BeforeEach
    fun setup() {
        MockKAnnotations.init(this)
        service = ProdutoService(apiClient)
    }

    @Test
    fun `deve buscar produtos da API`() = runTest {
        val produtosEsperados = listOf(
            ProdutoDto(1L, "Teclado", 150.0),
            ProdutoDto(2L, "Mouse", 80.0)
        )
        coEvery { apiClient.buscarProdutos() } returns produtosEsperados

        val resultado = service.carregarProdutos()

        assertEquals(2, resultado.size)
        coVerify { apiClient.buscarProdutos() }
    }

    @Test
    fun `deve retornar lista vazia em caso de erro`() = runTest {
        coEvery { apiClient.buscarProdutos() } throws IOException("Sem conexao")

        val resultado = service.carregarProdutos()

        assertTrue(resultado.isEmpty())
    }
}

Note o uso de coEvery e coVerify para funcoes suspensas.

Testes com Kotest

O Kotest oferece uma alternativa ao JUnit com estilos de teste inspirados em frameworks como RSpec e ScalaTest:

class ProdutoServiceKotest : FunSpec({

    val repository = mockk<ProdutoRepository>()
    val service = ProdutoService(repository)

    beforeTest {
        clearAllMocks()
    }

    test("deve listar todos os produtos") {
        val produtos = listOf(
            Produto(1L, "Teclado", 150.0),
            Produto(2L, "Mouse", 80.0)
        )
        every { repository.listarTodos() } returns produtos

        val resultado = service.listarTodos()

        resultado shouldHaveSize 2
        resultado.first().nome shouldBe "Teclado"
    }

    test("deve filtrar produtos por faixa de preco") {
        val produtos = listOf(
            Produto(1L, "Teclado", 150.0),
            Produto(2L, "Mouse", 80.0),
            Produto(3L, "Monitor", 1200.0)
        )
        every { repository.listarTodos() } returns produtos

        val resultado = service.filtrarPorPreco(min = 100.0, max = 500.0)

        resultado shouldHaveSize 1
        resultado.first().nome shouldBe "Teclado"
    }

    context("ao criar um produto") {
        test("deve salvar no repositorio") {
            val novoProduto = CriarProdutoRequest("Webcam", 250.0)
            every { repository.salvar(any()) } returns Produto(3L, "Webcam", 250.0)

            val resultado = service.criar(novoProduto)

            resultado.id shouldBeGreaterThan 0L
            verify { repository.salvar(any()) }
        }

        test("deve rejeitar nome vazio") {
            val request = CriarProdutoRequest("", 100.0)

            shouldThrow<ValidacaoException> {
                service.criar(request)
            }
        }
    }
})

Testes de Integracao

Testes de integracao verificam a interacao entre componentes reais. Com Spring Boot:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProdutoIntegracaoTest {

    @Autowired
    private lateinit var restTemplate: TestRestTemplate

    @Autowired
    private lateinit var repository: ProdutoRepository

    @BeforeEach
    fun setup() {
        repository.deleteAll()
    }

    @Test
    fun `deve criar e buscar produto via API`() {
        // Criar
        val request = CriarProdutoRequest("Teclado", BigDecimal("150.00"))
        val criarResponse = restTemplate.postForEntity(
            "/api/produtos", request, ProdutoResponse::class.java
        )

        assertEquals(HttpStatus.CREATED, criarResponse.statusCode)
        assertNotNull(criarResponse.body?.id)

        // Buscar
        val id = criarResponse.body!!.id
        val buscarResponse = restTemplate.getForEntity(
            "/api/produtos/$id", ProdutoResponse::class.java
        )

        assertEquals(HttpStatus.OK, buscarResponse.statusCode)
        assertEquals("Teclado", buscarResponse.body?.nome)
    }

    @Test
    fun `deve retornar 404 para produto inexistente`() {
        val response = restTemplate.getForEntity(
            "/api/produtos/999", ErroResponse::class.java
        )

        assertEquals(HttpStatus.NOT_FOUND, response.statusCode)
    }
}

Test Fixtures e Builders

Para manter testes limpos, crie fixtures e builders reutilizaveis:

object ProdutoFixture {

    fun umProduto(
        id: Long = 1L,
        nome: String = "Produto Teste",
        preco: Double = 100.0,
        descricao: String? = "Descricao padrao"
    ) = Produto(
        id = id,
        nome = nome,
        preco = preco,
        descricao = descricao
    )

    fun listaDeProdutos(quantidade: Int = 5): List<Produto> {
        return (1..quantidade).map { i ->
            umProduto(id = i.toLong(), nome = "Produto $i", preco = i * 50.0)
        }
    }
}

// Uso no teste
@Test
fun `deve calcular total do carrinho`() {
    val produtos = ProdutoFixture.listaDeProdutos(3)
    val carrinho = Carrinho(produtos)

    assertEquals(300.0, carrinho.total(), 0.01)
}

Boas Praticas para Testes em Kotlin

  • Nomeie testes descritivamente: use backticks para nomes em portugues que expliquem o cenario: deve retornar erro quando email invalido.
  • Siga o padrao AAA: Arrange (preparar), Act (executar), Assert (verificar) em cada teste.
  • Um assert por teste quando possivel: testes focados sao mais faceis de debugar quando falham.
  • Evite logica nos testes: testes nao devem ter if, for ou logica complexa. Devem ser lineares e previsiveis.
  • Use fixtures e builders: evite duplicacao de setup entre testes.
  • Teste comportamento, nao implementacao: verifique o resultado, nao como ele foi obtido internamente.
  • Mantenha testes rapidos: testes unitarios devem executar em milissegundos. Isole I/O com mocks.

Erros Comuns e Armadilhas

  • Mocks excessivos: se voce precisa mockar muitas dependencias, provavelmente a classe tem responsabilidades demais. Refatore.
  • Testes frageis: testes que quebram com qualquer refatoracao interna estao acoplados a implementacao, nao ao comportamento.
  • Nao limpar estado entre testes: use @BeforeEach para garantir isolamento. Testes nao devem depender da ordem de execucao.
  • Ignorar testes de borda: alem do caso feliz, teste entradas vazias, nulas, limites numericos e cenarios de erro.
  • Esquecer runTest para coroutines: usar runBlocking em testes de coroutines nao avanca o tempo virtual. Use runTest do kotlinx-coroutines-test.
  • Testes de integracao sem limpeza: sempre limpe o banco de dados antes de cada teste de integracao para garantir resultados deterministicos.

Conclusao e Proximos Passos

Uma suite de testes bem estruturada e o investimento mais importante que voce pode fazer em um projeto. Com JUnit 5, MockK e Kotest, o Kotlin oferece ferramentas para escrever testes claros, concisos e confiaveis. Comece com testes unitarios para a logica de negocio, adicione testes de integracao para endpoints e repositorios e evolua para testes end-to-end quando necessario. Explore nossos guias sobre CI/CD e Clean Architecture para integrar testes ao seu pipeline de desenvolvimento e construir aplicacoes com confianca.