Por que os testes comuns mentem e você acredita neles
Imagine: você escreveu uma função, cobriu-a com três testes unitários. Marca verde. CI passa. Produção — bug. Familiar?
O problema dos testes unitários clássicos é que eles verificam exemplos específicos. Você mesmo inventa os dados de entrada e escreve o resultado esperado. É como um exame onde o aluno cria suas próprias questões. Mais cedo ou mais tarde, você vai perder um caso limite — null, lista vazia, número negativo ou string com caracteres Unicode.
É aqui que entra o teste baseado em propriedades (ou property-based testing). Em vez de escrever: "se passar 5, deve retornar 25", você formula uma propriedade: "para qualquer número inteiro, o quadrado é sempre não negativo". E o framework gera centenas de valores aleatórios e verifica essa propriedade.
Neste artigo, vamos explorar como usar testes baseados em propriedades em Kotlin com a biblioteca Kotest. Você aprenderá a escrever testes robustos, encontrar bugs em estágios iniciais e parar de ter medo de refatoração. Isso não é apenas teoria chata — vamos escrever código real que pode ser aplicado imediatamente em um projeto.
O que é property-based testing e por que Kotlin?
A ideia não é nova — foi popularizada pela biblioteca QuickCheck para Haskell em 1999. Mas só agora, com o aumento da complexidade dos sistemas backend e microsserviços, o teste baseado em propriedades está se tornando um must-have no stack do desenvolvedor.
Kotlin é a linguagem ideal para essa abordagem. Por quê?
- Recursos funcionais: lambdas, funções de extensão, imutabilidade — tudo isso simplifica a escrita de propriedades limpas.
- Kotest — um framework poderoso que suporta nativamente testes baseados em propriedades, geradores e Shrinking (algoritmo de minimização do exemplo de falha).
- Compatibilidade com Java: você pode testar código legado escrito em Java usando as mesmas abordagens.
Vamos direto ao ponto. Abaixo está um exemplo que vai mudar sua visão sobre testes.
Primeiro teste baseado em propriedades em 5 minutos
Suponha que temos uma função que verifica se uma string é um palíndromo:
fun isPalindrome(s: String): Boolean { val cleaned = s.filter { it.isLetterOrDigit() }.lowercase() return cleaned == cleaned.reversed()}Um teste comum se parece com isso:
@Testfun `test known palindromes`() { assertTrue(isPalindrome("A man, a plan, a canal: Panama")) assertTrue(isPalindrome("racecar")) assertFalse(isPalindrome("hello"))}Tudo funciona. Mas e se os dados de entrada contiverem espaços, números ou símbolos? E se a string estiver vazia? E se consistir apenas de espaços?
Agora vamos escrever um teste baseado em propriedades usando Kotest:
import io.kotest.core.spec.style.StringSpecimport io.kotest.property.forAllimport io.kotest.property.arbitrary.string
class PalindromePropertyTest : StringSpec({ "Qualquer string limpa deve ser um palíndromo de si mesma" { forAll<String> { original -> val cleaned = original.filter { it.isLetterOrDigit() }.lowercase() isPalindrome(cleaned) == true } }})O que está acontecendo aqui? forAll<String> gera strings aleatórias: vazias, com espaços, com Unicode, com números. E para cada uma, verifica a propriedade. Se qualquer string violar a afirmação — o teste falha, e o Kotest mostra o exemplo mínimo reproduzível.
Já nesta etapa, você pode encontrar bugs. Por exemplo, se a string consistir apenas de espaços — filter retornará uma string vazia, e isPalindrome("") retornará true. Um teste comum teria deixado passar.