Fuzzy testing

Adicionado ao Go 1.18, essa nova feature para testes promete ajudar a melhorar muito nosso código, já que com ela conseguimos testar inputs diferentes do que adicionamos em nossos testes, cobrindo assim uma gama muito maior de possibilidades.

Antes de continuar, se você caiu aqui mas prefere ver esse tutorial em vídeo, vou deixar aqui o link para um vídeo do nosso canal no YouTube onde mostramos essa belezinha em ação => Como implementar Fuzzy Test em Go.

Continuando….

Vamos imaginar que temos a seguinte função implementada.

package reverse

import (
    "errors"
    "unicode/utf8"
)

func Reverse(s string) (string, error) {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }

    return string(r), nil
}

Essa função basicamente inverte a ordem das letras de uma string, ou seja, um input tiago retornará ogait.

Antes de escrever um teste usando Fuzzy, vamos escrever um teste normal.

func TestReverse(t *testing.T) {
    testcases := []string{"Tiago Temporin", " ", "!33241"}
    for _, orig := range testcases {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }

        rev2, err2 := Reverse(rev)
        if err2 != nil {
            return
        }

        if orig != rev2 {
            t.Errorf("esperado: %q, obtido: %q", orig, rev2)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("a string reversa não é UTF-8: %q", rev)
        }
    }
}

Com o teste escrito, ao executar o comando go test . -v devemos ter um retorno parecido com isso:

➜ go test . -v
=== RUN   TestReverse
--- PASS: TestReverse (0.00s)
PASS
ok      github.com/aprendagolang/fuzz   0.009s

Show! Isso quer dizer que nosso código está funcionando perfeitamente como esperado.

Agora, vamos escrever um Fuzzy Test e ver se ele também irá passar ou se pode haver algum tipo de bug no nosso código que não conseguimos cobrir com nosso teste atual.

func FuzzReverse(f *testing.F) {
    testcases := []string{"Tiago Temporin", " ", "!33241"}
    for _, c := range testcases {
        f.Add(c)
    }

    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }

        rev2, err2 := Reverse(rev)
        if err2 != nil {
            return
        }

        if orig != rev2 {
            t.Errorf("esperado: %q, obtido: %q", orig, rev2)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("a string reversa não é UTF-8: %q", rev)
        }
    })
}

Como podemos ver acima, existem 3 pontos de atenção quando escrevemos um teste do tipo Fuzzy.

  1. O parâmetro da função é *testing.F e não *testing.T;
  2. O looping com f.Add dentro, serve como base para o fuzzy entender o tipo de input que ele precisa “mexer/bagunçar”;
  3. f.Fuzz recebe uma função onde o primeiro parâmetro é um *testing.T, e os demais são os que foram adicionados ao f.Add;
  4. O restante da função é igual a anterior.

Para que não fique nenhuma dúvida, sobre o f.Add e a função do f.Fuzz, vamos supor que você vá testar uma função com 3 parâmetros, sendo os 2 primeiros do tipo string e o terceiro do tipo uint8.

Nesse cenário, o f.Add ficaria parecido com isso:

testcases := []struct{
    Nome string
    Sobrenome string
    Idade uint8
}{
 {"Tiago", "Temporin", 32},
 {"Felipe", "Silva", 27},
}
for _, c := range testcases {
    f.Add(c.Nome, c.Sobrenome, c.Idade)
}

Já a função do f.Fuzz ficaria assim:

f.Fuzz(func(t *testing.T, nome, sobrenome string, idade uint8) {

Bom, voltando ao nosso teste, chegou a hora de executar e ver se vai dar tudo certo. Para executar um teste Fuzzy, precisamos adicionar a flag -fuzz ao nosso comando anterior.

➜ go test -fuzz . -v
=== RUN   TestReverse
--- PASS: TestReverse (0.00s)
=== FUZZ  FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/39 completed
failure while testing seed corpus entry: FuzzReverse/c600bf7f580fb7c074ebf53f34da1573a565d10811da6f71ab5907862bdaa7a5
fuzz: elapsed: 0s, gathering baseline coverage: 0/39 completed
--- FAIL: FuzzReverse (0.06s)
    --- FAIL: FuzzReverse (0.00s)
        reverse_test.go:48: esperado: "\xfb", obtido: "�"
    
FAIL
exit status 1
FAIL    github.com/aprendagolang/fuzz   0.080s

Como podemos ver, um erro ocorreu. Isso aconteceu por que não estamos validando se a string é utf-8, o que pode gerar um erro ao tentar inverter ela.

Para resolver isso, vamos editar nossa função Reverse e adicionar uma condição logo no inicio.

func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("a string passada não é UTF-8 valida")
    }

    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }

    return string(r), nil
}

Para verificar se isso resolve o erro que tivemos, vamos executar o teste anterior novamente passando a hash que obtivemos na execução.

➜ go test -run=FuzzReverse/c600bf7f580fb7c074ebf53f34da1573a565d10811da6f71ab5907862bdaa7a5 -v

=== RUN   FuzzReverse
=== RUN   FuzzReverse/c600bf7f580fb7c074ebf53f34da1573a565d10811da6f71ab5907862bdaa7a5
--- PASS: FuzzReverse (0.00s)
    --- PASS: FuzzReverse/c600bf7f580fb7c074ebf53f34da1573a565d10811da6f71ab5907862bdaa7a5 (0.00s)
PASS
ok      github.com/aprendagolang/fuzz   0.009s

Resolvido! Agora podemos executar o go test -fuzz . -v novamente para garantir que não há mais exceções para serem tratadas.

Deixem suas dúvidas nos comentários.

Até a próxima!


Subscreva

Fique por dentro de tudo o que acontece no mundo Go.

Deixe uma resposta