Introdução: Por que testes avançados são a base de um código Kotlin confiável
No desenvolvimento moderno com Kotlin, escrever código é apenas metade do trabalho. A verdadeira maestria se revela na capacidade de criar sistemas confiáveis, testáveis e fáceis de manter. Testes unitários básicos são familiares a todos, mas quando o projeto cresce para uma arquitetura de microsserviços ou lógica de negócios complexa, verificações simples não são mais suficientes. Testes avançados incluem não apenas o isolamento de classes, mas também a verificação da interação entre componentes (testes de integração) e a reprodução completa de cenários de usuário (E2E).
Este artigo é destinado a desenvolvedores que já conhecem os fundamentos do Kotlin e desejam elevar a qualidade de seus projetos a um novo patamar. Abordaremos três níveis-chave de teste: unit, integration e e2e, utilizando ferramentas modernas: MockK, Testcontainers e Ktor Client. Você aprenderá a escrever testes que não apenas "passam", mas que realmente protegem seu código contra regressões.
1. Testes Unitários: Isolamento e Mocks com MockK
Testes unitários são a base da pirâmide de testes. Em um nível avançado, não apenas verificamos funções, mas testamos interações complexas entre objetos, usando mocks. Em Kotlin, a melhor biblioteca para isso é o MockK.
1.1. Recursos Avançados do MockK
Ao contrário do Mockito, o MockK foi escrito especificamente para Kotlin e suporta corrotinas, lambdas e funções de extensão. Vejamos um exemplo de teste de um serviço que usa um repositório e corrotinas:
// Serviço para testarclass UserService(private val repo: UserRepository) { suspend fun getUserName(id: Long): String { val user = repo.findById(id) return user?.name ?: "Unknown" }}
// Testeclass UserServiceTest { private val repo = mockk<UserRepository>() private val service = UserService(repo)
@Test fun `deve retornar o nome do usuário quando o usuário existe`() = runTest { // given coEvery { repo.findById(1L) } returns User(1, "Alice")
// when val result = service.getUserName(1L)
// then assertEquals("Alice", result) coVerify(exactly = 1) { repo.findById(1L) } }
@Test fun `deve retornar Unknown quando o usuário não for encontrado`() = runTest { // given coEvery { repo.findById(any()) } returns null
// when val result = service.getUserName(99L)
// then assertEquals("Unknown", result) }}Pontos-chave:
coEveryecoVerify— para trabalhar com funções suspend.mockk()cria um mock vazio,spyk()— um mock parcial de um objeto real.- Use
slotspara capturar argumentos.
1.2. Testando Lambdas e Funções de Extensão
O MockK permite mockar funções de extensão, o que é crítico para frameworks como Ktor ou Spring WebFlux:
class StringValidator { fun String.isValidEmail(): Boolean = this.contains("@")}
// Teste@Testfun `teste de mock de extensão`() { val validator = mockk<StringValidator>() with(validator) { every { "test@mail.com".isValidEmail() } returns true every { "invalid".isValidEmail() } returns false } assertTrue("test@mail.com".isValidEmail()) assertFalse("invalid".isValidEmail())}2. Testes de Integração: Banco de Dados e Testcontainers
Testes de integração verificam como seu código interage com dependências reais: bancos de dados, filas de mensagens, APIs externas. É aqui que entra o Testcontainers — uma biblioteca que sobe contêineres Docker diretamente durante os testes.
2.1. Configuração do Testcontainers com PostgreSQL
Para uma aplicação Spring Boot, fica assim:
Blogs
Book Recommendations
Leia também
Programação Funcional em Swift: Fundamentos e Exemplos
Código Limpo em Rust: princípios e exemplos na prática
Quem, diabos, são Fn, FnMut e FnOnce? Closures do Rust sob o microscópio
Code Review em Rust: o que verificar e como dar feedback para iniciantes
Removendo chave de map em Go: o que acontece internamente?