28/100 Dias de Golang - Testes em Go com Fuzzing
Table of Contents
#
Testes em Go com Fuzzing
Já vimos que é muito importante termos testes de integração nos nossos códigos, vimos também sobre cobertura de código e comentamos que mesmo que um código esteja com 100% de cobertura não é garantia que ele esteja correto, lembre da frase do Dijkstra “Os testes mostram a presença, não a ausência de bugs”. É aqui que entra o fuzzing, uma técnica poderosa para encontrar falhas inesperadas no código. Desde a versão 1.18, o Go oferece suporte nativo ao fuzzing, tornando essa abordagem mais acessível para desenvolvedores.
Fuzzing é uma técnica de teste automatizado que consiste em fornecer entradas “aleatórias"para uma função, com o objetivo de encontrar comportamentos inesperados, como falhas, pânico ou resultados incorretos. Diferentemente dos testes tradicionais, onde você define entradas específicas e verifica os resultados esperados, o fuzzing explora automaticamente uma ampla gama de entradas, incluindo casos extremos e valores que você talvez não tenha considerado.
Vamos criar uma função que faz uma validação de email:
func ValidaEmail(email string) bool {
partes := strings.Split(email, "@")
if len(partes) != 2 {
return false
}
if !strings.Contains(partes[1], ".") {
return false
}
return true
}
Agora vamos criar um teste com Fuzzing.
func FuzzValidaEmail(f *testing.F) {
f.Add("user@example.com")
f.Add("user.name@domain.com")
f.Add("@example.com")
f.Add("user@")
f.Add("user@com")
f.Add("")
f.Add("user@domain..com")
f.Fuzz(func(t *testing.T, email string) {
result := ValidaEmail(email)
if email == "user@example.com" && !result {
t.Errorf("Espero email válido, mas recebi inválido: %s", email)
}
if email == "@example.com" && result {
t.Errorf("Esperava um email inválido, mas recebi válido: %s", email)
}
if email == "user@" && result {
t.Errorf("Esperava um email inválido, mas recebi válido: %s", email)
}
if email == "user@com" && result {
t.Errorf("Esperava um email inválido, mas recebi válido: %s", email)
}
})
}
Primeira alteração, na definição da função. Veja que passamos o f *testing.F
func FuzzValidaEmail(f *testing.F) {}
Depois adicionamos casos base para o fuzzing. O método f.Add
permite adicionar entradas iniciais que o fuzzer usará como ponto de partida. Isso ajuda a guiar o processo de fuzzing.
f.Add("user@example.com")
f.Add("user.name@domain.com")
f.Add("@example.com")
f.Add("user@")
f.Add("user@com")
f.Add("")
f.Add("user@domain..com")
Aí criamos a função de fuzz. E passamos umpara o f.Fuzz
aqui é onde definimos a lógica de teste. Ela recebe entradas geradas automaticamente pelo fuzzer e verifica se a função testada se comporta corretamente.
f.Fuzz(func(t *testing.T, email string) {
result := IsValidEmail(email)
Depois disso fazemos as validações:
if email == "user@example.com" && !result {
t.Errorf("Espero email válido, mas recebi inválido: %s", email)
}
if email == "@example.com" && result {
t.Errorf("Esperava um email inválido, mas recebi válido: %s", email)
}
if email == "user@" && result {
t.Errorf("Esperava um email inválido, mas recebi válido: %s", email)
}
if email == "user@com" && result {
t.Errorf("Esperava um email inválido, mas recebi válido: %s", email)
}
})
Vamos rodar esse teste com o comando
go test -fuzz=FuzzValidaEmail
O fuzzer continuará gerando entradas até que seja interrompido (Ctrl+C) ou até que ele encontre um problema. Se um problema for encontrado, a entrada será salva em um arquivo.
fuzz: elapsed: 0s, gathering baseline coverage: 0/7 completed
failure while testing seed corpus entry: FuzzValidaEmail/seed#2
fuzz: elapsed: 0s, gathering baseline coverage: 0/7 completed
--- FAIL: FuzzValidaEmail (0.01s)
--- FAIL: FuzzValidaEmail (0.00s)
utils_test.go:91: Esperava um email inválido, mas recebi válido: @example.com
FAIL
exit status 1
FAIL firsttest 0.010s
Essa saída nos mostra que não estamos validando o tamano das partes antes e depois do split, vamos adicionar essa validação:
if len(partes[0]) == 0 || len(partes[1]) == 0 {
return false
}
Rodando novamente temos o seguinte resultado:
go test -fuzz=.
fuzz: elapsed: 0s, gathering baseline coverage: 0/14 completed
fuzz: elapsed: 0s, gathering baseline coverage: 14/14 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 133901 (44627/sec), new interesting: 0 (total: 14)
fuzz: elapsed: 6s, execs: 274108 (46727/sec), new interesting: 0 (total: 14)
fuzz: elapsed: 9s, execs: 393680 (39858/sec), new interesting: 0 (total: 14)
fuzz: elapsed: 12s, execs: 519208 (41840/sec), new interesting: 0 (total: 14)
fuzz: elapsed: 15s, execs: 649237 (43350/sec), new interesting: 0 (total: 14)
fuzz: elapsed: 18s, execs: 787556 (46102/sec), new interesting: 0 (total: 14)
^Cfuzz: elapsed: 20s, execs: 871096 (44884/sec), new interesting: 0 (total: 14)
PASS
ok firsttest 19.877s
Caso você não cancele com o Ctrl+C ele ficará rodando, podemos passar um tempos específico para ele rodar:
go test -fuzz=. --fuzztime=10s
fuzz: elapsed: 0s, gathering baseline coverage: 0/14 completed
fuzz: elapsed: 0s, gathering baseline coverage: 14/14 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 134818 (44927/sec), new interesting: 0 (total: 14)
fuzz: elapsed: 6s, execs: 259741 (41643/sec), new interesting: 0 (total: 14)
fuzz: elapsed: 9s, execs: 388032 (42762/sec), new interesting: 0 (total: 14)
fuzz: elapsed: 10s, execs: 431481 (40937/sec), new interesting: 0 (total: 14)
PASS
ok firsttest 10.067s
O fuzzing é uma ferramenta poderosa para melhorar a qualidade do software, e o suporte nativo no Go torna essa técnica mais acessível do que nunca!