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,forou 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
@BeforeEachpara 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
runTestpara coroutines: usarrunBlockingem testes de coroutines nao avanca o tempo virtual. UserunTestdokotlinx-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.