Pular para o conteúdo principal

victorstein.dev

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!