Skip to content

Testes

Para desenvolvedores, bons testes podem detectar erros no programa antecipadamente, evitando a carga mental causada por bugs devido à manutenção inadequada posteriormente. Portanto, escrever bons testes é muito necessário. Go fornece uma ferramenta de linha de comando muito simples e prática para testes chamada go test. Podemos ver testes na biblioteca padrão e em muitos frameworks open source. Esta ferramenta é muito conveniente de usar e atualmente suporta os seguintes tipos de teste:

  • Teste de exemplo
  • Teste unitário
  • Teste de benchmark
  • Teste fuzzy

A maioria das APIs em Go é fornecida pela biblioteca padrão testing.

TIP

Execute o comando go help testfunc na linha de comando para ver a explicação oficial do Go para os quatro tipos de teste acima.

Convenções de Escrita

Antes de começar a escrever testes, primeiro precisamos prestar atenção a algumas convenções, o que tornará o aprendizado subsequente mais conveniente.

  • Pacote de teste: Arquivos de teste devem ser colocados em um pacote separado, este pacote é normalmente nomeado test.
  • Arquivo de teste: Arquivos de teste normalmente terminam com _test.go. Por exemplo, se quiser testar uma determinada função, nomeie-o como function_test.go. Se quiser dividir ainda mais de acordo com o tipo de teste, também pode usar o tipo de teste como prefixo do arquivo, como benchmark_marshaling_test.go ou example_marshaling_test.go.
  • Função de teste: Cada arquivo de teste terá várias funções de teste para diferentes testes. Para diferentes tipos de teste, o estilo de nomenclatura das funções de teste também é diferente. Por exemplo, teste de exemplo é ExampleXXXX, teste unitário é TestXXXX, teste de benchmark é BenchmarkXXXX, e teste fuzzy é FuzzXXXX. Assim, mesmo sem comentários, podemos saber que tipo de teste é.

TIP

Quando o nome do pacote é testdata, este pacote é normalmente usado para armazenar dados auxiliares para testes. Ao executar testes, o Go ignora pacotes chamados testdata.

Seguir as convenções acima e desenvolver um bom estilo de teste pode economizar muitos problemas para manutenção futura.

Executar Testes

O comando go test é usado principalmente para executar testes. Vamos usar código real como exemplo. Agora temos um arquivo a ser testado /say/hello.go com o seguinte código:

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

E o arquivo de teste /test/example_test.go com o seguinte código:

go
package test

import (
  "golearn/say"
)

func ExampleHello() {
  say.Hello()
  // Output:
  // hello
}

func ExampleGoodBye() {
  say.GoodBye()
  // Output:
  // bye
}

func ExampleSay() {
  say.Hello()
  say.GoodBye()
  // Output:
  // hello
  // bye
}

Existem várias maneiras de executar estes testes. Por exemplo, se quiser executar todos os casos de teste no pacote test, pode executar diretamente o seguinte comando no diretório test:

sh
$ go test ./
PASS
ok      golearn/test    0.422s

./ representa o diretório atual. O Go recompilará todos os arquivos de teste no diretório test e executará todos os casos de teste. Dos resultados, podemos ver que todos os casos de teste passaram. Os parâmetros subsequentes também podem seguir vários diretórios. Por exemplo, o comando abaixo mostra que o diretório principal do projeto não tem arquivos de teste para executar:

sh
$ go test ./ ../
ok      golearn/test
?       golearn [no test files]

TIP

Quando os parâmetros de execução têm múltiplos pacotes, o Go não executará novamente os casos de teste que já passaram com sucesso. Durante a execução, (cached) será adicionado ao final da linha para indicar que o resultado de saída é do cache da execução anterior. Quando os parâmetros de sinalizador de teste estão no seguinte conjunto, o Go armazenará em cache os resultados do teste, caso contrário, não:

-benchtime, -cpu, -list, -parallel, -run, -short, -timeout, -failfast, -v

Se quiser desativar o cache, pode adicionar o parâmetro -count=1.

Claro, também é possível especificar um determinado arquivo de teste para executar:

sh
$ go test example_test.go
ok      command-line-arguments  0.457s

Ou pode especificar um determinado caso de teste em um determinado arquivo de teste, por exemplo:

sh
$ go test -run ExampleSay
PASS
ok      golearn/test    0.038s

Embora as três situações acima tenham completado os testes, os resultados de saída são muito concisos. Neste ponto, podemos adicionar o parâmetro -v para tornar os resultados de saída mais detalhados, por exemplo:

sh
$ go test ./ -v
=== RUN   ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN   ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN   ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok      golearn/test    0.040s

Agora podemos ver claramente a ordem de execução, tempo de execução, status de execução de cada caso de teste, bem como o tempo total de execução.

TIP

O comando go test executa por padrão todos os testes unitários, testes de exemplo e testes fuzzy. Se adicionar o parâmetro -bench, executará todos os tipos de teste, como o comando abaixo:

sh
$ go test -bench .

Portanto, é necessário usar o parâmetro -run para especificar. Por exemplo, o comando para executar apenas todos os testes de benchmark é:

sh
$ go test -bench . -run ^$

Parâmetros Comuns

Os testes em Go têm muitos parâmetros de sinalizador. Abaixo apresentaremos apenas os parâmetros comumente usados. Para mais detalhes, recomenda-se usar o comando go help testflag para consultar.

ParâmetroDescrição
-o fileEspecifica o nome do arquivo binário após compilação
-cCompila apenas arquivos de teste, mas não executa
-jsonOutput de logs de teste em formato json
-exec xprogExecuta testes usando xprog, equivalente a go run
-bench regexpSeleciona testes de benchmark que correspondem a regexp
-fuzz regexpSeleciona testes fuzzy que correspondem a regexp
-fuzztime tTempo para o teste fuzzy terminar automaticamente, t é o intervalo de tempo. Quando a unidade é x, representa o número de vezes, por exemplo 200x
-fuzzminimizetime tTempo mínimo de execução do teste de minimização, mesma regra acima
-count nExecuta testes n vezes, padrão é 1 vez
-coverAtiva análise de cobertura de teste
-covermode set,count,atomicDefine o modo de análise de cobertura
-cpuExecuta GOMAXPROCS para testes
-failfastApós a primeira falha de teste, não inicia novos testes
-list regexpLista casos de teste que correspondem a regexp
-parallel nPermite que casos de teste que chamaram t.Parallel sejam executados em paralelo, n é o número máximo de paralelismo
-run regexpExecuta apenas casos de teste que correspondem a regexp
-skip regexpPula casos de teste que correspondem a regexp
-timeout dSe o tempo de execução de um único teste exceder o intervalo de tempo d, ocorrerá panic. d é o intervalo de tempo, por exemplo 1s, 1ms, 1ns, etc.
-shuffle off,on,NEmbaralha a ordem de execução dos testes, N é a semente aleatória, a semente padrão é o tempo do sistema
-vOutput de logs de teste mais detalhados
-benchmemEstatísticas de alocação de memória para testes de benchmark
-blockprofile block.outEstatísticas de bloqueio de goroutines durante testes e grava em arquivo
-blockprofilerate nControla a frequência de estatísticas de bloqueio de goroutines. Veja mais detalhes através do comando go doc runtime.SetBlockProfileRate
-coverprofile cover.outEstatísticas de cobertura de teste e grava em arquivo
-cpuprofile cpu.outEstatísticas de CPU e grava em arquivo
-memprofile mem.outEstatísticas de alocação de memória e grava em arquivo
-memprofilerate nControla a frequência de estatísticas de alocação de memória. Veja mais detalhes através do comando go doc runtime.MemProfileRate
-mutexprofile mutex.outEstatísticas de contenção de lock e grava em arquivo
-mutexprofilefraction nDefine estatísticas de n goroutines competindo por um mutex
-trace trace.outGrava o rastreamento de execução em arquivo
-outputdir directoryEspecifica o diretório de saída para os arquivos de estatística acima, o padrão é o diretório de execução do go test

Teste de Exemplo

O teste de exemplo não é como os outros três tipos de teste para descobrir problemas no programa. Ele é mais para mostrar como usar uma determinada função, servindo como documentação. O teste de exemplo não é um conceito definido oficialmente, nem é uma norma rígida. É mais uma convenção de engenharia, e se seguir ou não depende do desenvolvedor. Testes de exemplo aparecem com muita frequência na biblioteca padrão, normalmente são exemplos de código da biblioteca padrão escritos oficialmente. Por exemplo, a função de teste ExampleWithDeadline na biblioteca padrão context/example_test.go mostra o uso básico de DeadlineContext:

go
// This example passes a context with an arbitrary deadline to tell a blocking
// function that it should abandon its work as soon as it gets to it.
func ExampleWithDeadline() {
   d := time.Now().Add(shortDuration)
   ctx, cancel := context.WithDeadline(context.Background(), d)

   // Even though ctx will be expired, it is good practice to call its
   // cancellation function in any case. Failure to do so may keep the
   // context and its parent alive longer than necessary.
   defer cancel()

   select {
   case <-time.After(1 * time.Second):
      fmt.Println("overslept")
   case <-ctx.Done():
      fmt.Println(ctx.Err())
   }

   // Output:
   // context deadline exceeded
}

Superficialmente, esta função de teste é apenas uma função comum, mas o teste de exemplo é principalmente refletido pelo comentário Output. Quando a função a ser testada tem apenas uma linha de saída, use o comentário Output para detectar a saída. Primeiro, crie um arquivo chamado hello.go e escreva o seguinte código:

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

A função SayHello é a função a ser testada. Em seguida, crie o arquivo de teste example_test.go e escreva o seguinte código:

go
package test

import (
  "golearn/say"
)

func ExampleHello() {
  say.Hello()
  // Output:
  // hello
}

func ExampleGoodBye() {
  say.GoodBye()
  // Output:
  // bye
}

func ExampleSay() {
  say.Hello()
  say.GoodBye()
  // Output:
  // hello
  // bye
}

O comentário Output na função indica que a saída da função de teste deve ser hello. Em seguida, execute o comando de teste para ver o resultado:

sh
$ go test -v
=== RUN   ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN   ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN   ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok      golearn/test    0.448s

Dos resultados, podemos ver que todos os testes passaram. Sobre Output, existem as seguintes formas de escrita:

A primeira é ter apenas uma linha de saída, significando detectar se a saída da função é hello:

// Output:
// hello

A segunda é ter múltiplas linhas de saída, ou seja, detectar se a saída corresponde em ordem:

// Output:
// hello
// bye

A terceira é saída não ordenada, ou seja, múltiplas linhas de saída sem seguir a ordem:

// Unordered output:
// bye
// hello

Vale notar que, para funções de teste, apenas quando as últimas linhas são comentários Output elas serão consideradas como teste de exemplo. Caso contrário, é apenas uma função comum e não será executada pelo Go.

Teste Unitário

O teste unitário testa a menor unidade testável no software. O tamanho da unidade depende do desenvolvedor, pode ser uma struct, um pacote, uma função ou um tipo. Vamos continuar demonstrando com exemplos. Primeiro, crie o arquivo /tool/math.go e escreva o seguinte código:

go
package tool

type Number interface {
  ~int8 | ~int16 | ~int32 | ~int64 | ~int
}

func SumInt[T Number](a, b T) T {
  return a + b
}

func Equal[T Number](a, b T) bool {
  return a == b
}

Em seguida, crie o arquivo de teste /tool_test/unit_test.go. Para testes unitários, a nomenclatura pode ser unit_test ou usar o pacote ou função que deseja testar como prefixo do arquivo:

go
package test_test

import (
  "golearn/tool"
  "testing"
)

func TestSum(t *testing.T) {
  a, b := 10, 101
  expected := 111

  actual := tool.SumInt(a, b)
  if actual != expected {
    t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
  }
}

func TestEqual(t *testing.T) {
  a, b := 10, 101
  expected := false

  actual := tool.Equal(a, b)
  if actual != expected {
    t.Errorf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
}

Para testes unitários, o estilo de nomenclatura de cada caso de teste é TestXXXX, e o parâmetro de entrada da função deve ser t *testing.T. testing.T é uma struct fornecida pelo pacote testing para facilitar testes, fornecendo muitos métodos disponíveis. No exemplo, t.Errorf é equivalente a t.Logf, usado para formatar e output informações de log de falha de teste. Outros comumente usados incluem t.Fail para marcar o caso atual como falha de teste. Funções semelhantes incluem t.FailNow, que também marca como falha de teste, mas o primeiro continua a executar após a falha, enquanto o último para a execução diretamente. Veja o exemplo abaixo, modificando o resultado esperado para um resultado errado:

go
package tool_test

import (
  "golearn/tool"
  "testing"
)

func TestSum(t *testing.T) {
  a, b := 10, 101
  expected := 110

  actual := tool.SumInt(a, b)
  if actual != expected {
        // Errorf usa internamente t.Fail()
    t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
  }
  t.Log("test finished")
}

func TestEqual(t *testing.T) {
  a, b := 10, 101
  expected := true

  actual := tool.Equal(a, b)
  if actual != expected {
        // Fatalf usa internamente t.FailNow()
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
  t.Log("test finished")
}

Executar o teste acima produz o seguinte output:

sh
$ go test tool_test.go -v
=== RUN   TestSum
    tool_test.go:14: Sum(10,101) expected 110,actual is 111
    tool_test.go:16: test finished
--- FAIL: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL    command-line-arguments  0.037s

Do log de teste, podemos ver que o caso TestSum outputou "test finished" mesmo após falhar, enquanto TestEqual não. Da mesma forma, t.SkipNow marcará o caso atual como SKIP e parará a execução, continuando na próxima rodada de testes.

go
package tool_test

import (
   "golearn/tool"
   "testing"
)

func TestSum(t *testing.T) {
   a, b := 10, 101
   expected := 110

   actual := tool.SumInt(a, b)
   if actual != expected {
      t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
   }
   t.Log("test finished")
}

func TestEqual(t *testing.T) {
   a, b := 10, 101
   expected := true

   actual := tool.Equal(a, b)
   if actual != expected {
      t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
   }
   t.Log("test finished")
}

Ao executar o teste, modifique o número de execuções para 2:

sh
$ go test tool_test.go -v -count=2
=== RUN   TestSum
    tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
=== RUN   TestSum
    tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL    command-line-arguments  0.468s

No exemplo acima, "test finished" é outputado na última linha para indicar que o teste foi concluído. Na verdade, podemos usar t.Cleanup para registrar uma função de finalização para fazer isso. Esta função será executada quando o caso de teste terminar, como segue:

go
package tool_test

import (
  "golearn/tool"
  "testing"
)

func finished(t *testing.T) {
  t.Log("test finished")
}

func TestSum(t *testing.T) {
  t.Cleanup(func() {
    finished(t)
  })

  a, b := 10, 101
  expected := 111

  actual := tool.SumInt(a, b)
  if actual != expected {
    t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
  }

}

func TestEqual(t *testing.T) {
  t.Cleanup(func() {
    finished(t)
  })

  a, b := 10, 101
  expected := false

  actual := tool.Equal(a, b)
  if actual != expected {
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
}

Executar o teste produz o seguinte output:

sh
$ go test tool_test.go -v
=== RUN   TestSum
    tool_test.go:9: test finished
--- PASS: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:9: test finished
--- PASS: TestEqual (0.00s)
PASS
ok      command-line-arguments  0.462s

Helper

Através de t.Helper(), podemos marcar a função atual como uma função auxiliar. Funções auxiliares não são executadas como casos de teste individuais. Ao registrar logs, o número da linha outputado é o número da linha do chamador da função auxiliar. Isso torna a análise de logs mais precisa, evitando informações冗杂as desnecessárias. Por exemplo, o exemplo t.Cleanup acima pode ser modificado para uma função auxiliar, como segue:

go
package tool_test

import (
   "golearn/tool"
   "testing"
)

func CleanupHelper(t *testing.T) {
   t.Helper()
   t.Log("test finished")
}

func TestSum(t *testing.T) {
   t.Cleanup(func() {
      CleanupHelper(t)
   })

   a, b := 10, 101
   expected := 111

   actual := tool.SumInt(a, b)
   if actual != expected {
      t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
   }

}

func TestEqual(t *testing.T) {
   t.Cleanup(func() {
      CleanupHelper(t)
   })

   a, b := 10, 101
   expected := false

   t.Helper()
   actual := tool.Equal(a, b)
   if actual != expected {
      t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
   }
}

Executar o teste produz as seguintes informações, a diferença em relação ao anterior é que o número da linha de "test finished" se torna o número da linha do chamador:

sh
$ go test tool_test.go -v
=== RUN   TestSum
    tool_test.go:15: test finished
--- PASS: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:30: test finished
--- PASS: TestEqual (0.00s)
PASS
ok      command-line-arguments  0.464s

TIP

As operações acima só podem ser realizadas no teste principal, ou seja, casos de teste executados diretamente. Se usados em subtestes, causarão panic.

Subtestes

Em alguns casos, pode ser necessário testar outros casos de teste dentro de um caso de teste. Esses casos de teste aninhados são geralmente chamados de subtestes. Através do método t.Run(), cuja assinatura é a seguinte:

go
// O método Run inicia uma nova goroutine para executar o subteste, bloqueia e espera até que a função f termine antes de retornar
// O valor de retorno é se o teste passou
func (t *T) Run(name string, f func(t *T)) bool

Aqui está um exemplo:

go
func TestTool(t *testing.T) {
  t.Run("tool.Sum(10,101)", TestSum)
  t.Run("tool.Equal(10,101)", TestEqual)
}

Executar produz o seguinte resultado:

sh
$ go test -run TestTool -v
=== RUN   TestTool
=== RUN   TestTool/tool.Sum(10,101)
    tool_test.go:15: test finished
=== RUN   TestTool/tool.Equal(10,101)
    tool_test.go:30: test finished
--- PASS: TestTool (0.00s)
    --- PASS: TestTool/tool.Sum(10,101) (0.00s)
    --- PASS: TestTool/tool.Equal(10,101) (0.00s)
PASS
ok      golearn/tool_test       0.449s

Através do output, podemos ver claramente a estrutura hierárquica pai-filho. No exemplo acima, o segundo subteste não será executado até que o primeiro subteste seja concluído. Podemos usar t.Parallel() para marcar casos de teste como executáveis em paralelo, de modo que a ordem de output se torne indeterminada:

go
package tool_test

import (
  "golearn/tool"
  "testing"
)

func CleanupHelper(t *testing.T) {
  t.Helper()
  t.Log("test finished")
}

func TestSum(t *testing.T) {
  t.Parallel()
  t.Cleanup(func() {
    CleanupHelper(t)
  })

  a, b := 10, 101
  expected := 111

  actual := tool.SumInt(a, b)
  if actual != expected {
    t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
  }

}

func TestEqual(t *testing.T) {
  t.Parallel()
  t.Cleanup(func() {
    CleanupHelper(t)
  })

  a, b := 10, 101
  expected := false

  actual := tool.Equal(a, b)
  if actual != expected {
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
}

func TestToolParallel(t *testing.T) {
  t.Log("setup")
  t.Run("tool.Sum(10,101)", TestSum)
  t.Run("tool.Equal(10,101)", TestEqual)
  t.Log("teardown")
}

Executar o teste produz o seguinte output:

sh
$ go test -run TestTool -v
=== RUN   TestToolParallel
    tool_test.go:46: setup
=== RUN   TestToolParallel/tool.Sum(10,101)
=== PAUSE TestToolParallel/tool.Sum(10,101)
=== RUN   TestToolParallel/tool.Equal(10,101)
=== PAUSE TestToolParallel/tool.Equal(10,101)
=== NAME  TestToolParallel
    tool_test.go:49: teardown
=== CONT  TestToolParallel/tool.Sum(10,101)
=== CONT  TestToolParallel/tool.Equal(10,101)
=== NAME  TestToolParallel/tool.Sum(10,101)
    tool_test.go:16: test finished
=== NAME  TestToolParallel/tool.Equal(10,101)
    tool_test.go:32: test finished
--- PASS: TestToolParallel (0.00s)
    --- PASS: TestToolParallel/tool.Sum(10,101) (0.00s)
    --- PASS: TestToolParallel/tool.Equal(10,101) (0.00s)
PASS
ok      golearn/tool_test       0.444s

Do resultado do teste, podemos ver claramente que há um processo de bloqueio e espera. Ao executar casos de teste concorrentemente, o exemplo acima certamente não pode ser realizado normalmente, porque o código subsequente não pode garantir execução síncrona. Neste ponto, podemos escolher aninhar outra camada de t.Run(), como segue:

go
func TestToolParallel(t *testing.T) {
  t.Log("setup")
  t.Run("process", func(t *testing.T) {
    t.Run("tool.Sum(10,101)", TestSum)
    t.Run("tool.Equal(10,101)", TestEqual)
  })
  t.Log("teardown")
}

Executar novamente, podemos ver o resultado de execução normal:

sh
$ go test -run TestTool -v
=== RUN   TestToolParallel
    tool_test.go:46: setup
=== RUN   TestToolParallel/process
=== RUN   TestToolParallel/process/tool.Sum(10,101)
=== PAUSE TestToolParallel/process/tool.Sum(10,101)
=== RUN   TestToolParallel/process/tool.Equal(10,101)
=== PAUSE TestToolParallel/process/tool.Equal(10,101)
=== CONT  TestToolParallel/process/tool.Sum(10,101)
=== CONT  TestToolParallel/process/tool.Equal(10,101)
=== NAME  TestToolParallel/process/tool.Sum(10,101)
    tool_test.go:16: test finished
=== NAME  TestToolParallel/process/tool.Equal(10,101)
    tool_test.go:32: test finished
=== NAME  TestToolParallel
    tool_test.go:51: teardown
--- PASS: TestToolParallel (0.00s)
    --- PASS: TestToolParallel/process (0.00s)
        --- PASS: TestToolParallel/process/tool.Sum(10,101) (0.00s)
        --- PASS: TestToolParallel/process/tool.Equal(10,101) (0.00s)
PASS
ok      golearn/tool_test       0.450s

Estilo de Tabela

Nos testes unitários acima, os dados de entrada de teste são todos variáveis declaradas manualmente. Quando a quantidade de dados é pequena, não há problema, mas se quisermos testar múltiplos conjuntos de dados, não é mais viável declarar variáveis para criar dados de teste. Portanto, geralmente tentamos usar a forma de slice de structs, onde as structs são structs anônimas declaradas temporariamente. Como esse estilo de codificação parece uma tabela, é chamado de table-driven. Vamos dar um exemplo. Este é um exemplo de declaração manual de múltiplas variáveis para criar dados de teste. Se houver múltiplos conjuntos de dados, não parece muito intuitivo, então vamos modificá-lo para o estilo de tabela:

go
func TestEqual(t *testing.T) {
  t.Cleanup(func() {
    CleanupHelper(t)
  })

  a, b := 10, 101
  expected := false
  actual := tool.Equal(a, b)
  if actual != expected {
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
}

O código modificado é o seguinte:

go
func TestEqual(t *testing.T) {
  t.Cleanup(func() {
    CleanupHelper(t)
  })

  // table driven style
  testData := []struct {
    a, b int
    exp  bool
  }{
    {10, 101, false},
    {5, 5, true},
    {30, 32, false},
    {100, 101, false},
    {2, 3, false},
    {4, 4, true},
  }

  for _, data := range testData {
    if actual := tool.Equal(data.a, data.b); actual != data.exp {
      t.Fatalf("Sum(%d,%d) expected %t,actual is %t", data.a, data.b, data.exp, actual)
    }
  }
}

Estes dados de teste parecem muito mais intuitivos.

Teste de Benchmark

O teste de benchmark, também chamado de teste de desempenho, é normalmente usado para testar indicadores de desempenho como uso de memória, uso de CPU, tempo de execução, etc. Para testes de benchmark, os arquivos de teste normalmente terminam com bench_test.go, e as funções dos casos de teste devem estar no formato BenchmarkXXXX.

Vamos usar um exemplo de concatenação de strings para comparação de desempenho como exemplo de teste de benchmark. Primeiro, crie o arquivo /tool/strConcat.go. Como todos sabem, usar diretamente strings para concatenação com + tem desempenho muito baixo, enquanto usar strings.Builder é muito melhor. Crie duas funções no arquivo /tool/strings.go para concatenação de strings das duas maneiras:

go
package tool

import "strings"


func ConcatStringDirect(longString string) {
   res := ""
   for i := 0; i < 100_000.; i++ {
      res += longString
   }
}

func ConcatStringWithBuilder(longString string) {
   var res strings.Builder
   for i := 0; i < 100_000.; i++ {
      res.WriteString(longString)
   }
}

Em seguida, crie o arquivo de teste /tool_test/bench_tool_test.go com o seguinte código:

go
package tool_test

import (
  "golearn/tool"
  "testing"
)

var longString = "longStringlongStringlongStringlongStringlongStringlongStringlongStringlongString"

func BenchmarkConcatDirect(b *testing.B) {
  for i := 0; i < b.N; i++ {
    tool.ConcatStringDirect(longString)
  }
}

func BenchmarkConcatBuilder(b *testing.B) {
  for i := 0; i < b.N; i++ {
    tool.ConcatStringWithBuilder(longString)
  }
}

Executar o comando de teste, o comando ativa logs detalhados e análise de memória, especifica a lista de núcleos de CPU a serem usados, e cada caso de teste é executado duas vezes. O output é o seguinte:

sh
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=2
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2                4         277771375 ns/op        4040056736 B/op    10000 allocs/op
BenchmarkConcatDirect-2                4         278500125 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-4                1        1153796000 ns/op        4040068784 B/op    10126 allocs/op
BenchmarkConcatDirect-4                1        1211017600 ns/op        4040073104 B/op    10171 allocs/op
BenchmarkConcatDirect-8                2         665460800 ns/op        4040077760 B/op    10219 allocs/op
BenchmarkConcatDirect-8                2         679774450 ns/op        4040080064 B/op    10243 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2            3428            344530 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            3579            351858 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-4            2448            736177 ns/op         4128185 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1688            662993 ns/op         4128185 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1958            550333 ns/op         4128199 B/op         29 allocs/op
BenchmarkConcatBuilder-8            2174            552113 ns/op         4128196 B/op         29 allocs/op
PASS
ok      golearn/tool_test       21.381s

Abaixo explicamos o resultado de output do teste de benchmark. goos representa o sistema operacional em execução, goarch representa a arquitetura da CPU, pkg é o pacote onde o teste está localizado, e cpu são algumas informações sobre a CPU. O resultado de cada caso de teste abaixo é separado pelo nome de cada teste de benchmark. O 2 na primeira coluna BenchmarkConcatDirect-2 representa o número de núcleos de CPU usados, o 4 na segunda coluna representa o tamanho de b.N no código, que é o número de iterações no teste de benchmark. A terceira coluna 277771375 ns/op representa o tempo consumido por cada iteração, onde ns é nanossegundo. A quarta coluna 4040056736 B/op representa o tamanho em bytes de memória alocada por cada iteração. A quinta coluna 10000 allocs/op representa o número de alocações de memória por cada iteração.

Obviamente, de acordo com os resultados do teste, o desempenho de usar strings.Builder é muito superior ao de concatenar strings usando +. Comparar desempenho através de dados intuitivos é exatamente o propósito do teste de benchmark.

benchstat

benchstat é uma ferramenta de análise de teste de desempenho open source. O número de amostras de teste de desempenho acima é apenas dois grupos. Uma vez que o número de amostras aumenta, a análise manual se torna muito demorada e trabalhosa. Esta ferramenta nasceu para resolver problemas de análise de desempenho.

Primeiro, precisamos baixar a ferramenta:

sh
$ go install golang.org/x/perf/benchstat

Execute o teste de benchmark duas vezes, desta vez modifique o número de amostras para 5, e output para os arquivos old.txt e new.txt para comparação. O resultado da primeira execução:

sh
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a old.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2                4         290535650 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         298974625 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         299637800 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         276487000 ns/op        4040056784 B/op    10001 allocs/op
BenchmarkConcatDirect-2                4         356465275 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-4                2         894723200 ns/op        4040077424 B/op    10216 allocs/op
BenchmarkConcatDirect-4                2         785830400 ns/op        4040078288 B/op    10225 allocs/op
BenchmarkConcatDirect-4                2         743634000 ns/op        4040077568 B/op    10217 allocs/op
BenchmarkConcatDirect-4                2         953802700 ns/op        4040075408 B/op    10195 allocs/op
BenchmarkConcatDirect-4                2         953028750 ns/op        4040077520 B/op    10217 allocs/op
BenchmarkConcatDirect-8                2         684023150 ns/op        4040086784 B/op    10313 allocs/op
BenchmarkConcatDirect-8                2         634380250 ns/op        4040090528 B/op    10352 allocs/op
BenchmarkConcatDirect-8                2         685030600 ns/op        4040090768 B/op    10355 allocs/op
BenchmarkConcatDirect-8                2         817909650 ns/op        4040089808 B/op    10345 allocs/op
BenchmarkConcatDirect-8                2         600078100 ns/op        4040095664 B/op    10406 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2            2925            419651 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            2961            423899 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            2714            422275 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            2848            452255 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            2612            454452 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-4             974           1158000 ns/op         4128189 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1098           1068682 ns/op         4128192 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1042           1056570 ns/op         4128194 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1280            978213 ns/op         4128191 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1538           1162108 ns/op         4128190 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1744            700824 ns/op         4128203 B/op         29 allocs/op
BenchmarkConcatBuilder-8            2235            759537 ns/op         4128201 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1556            736455 ns/op         4128204 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1592            825794 ns/op         4128201 B/op         29 allocs/op
BenchmarkConcatBuilder-8            2263            717285 ns/op         4128203 B/op         29 allocs/op
PASS
ok      golearn/tool_test       56.742s

O resultado da segunda execução:

sh
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2                4         285074900 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         291517150 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         281901975 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         292320625 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         286723000 ns/op        4040056952 B/op    10002 allocs/op
BenchmarkConcatDirect-4                1        1188983000 ns/op        4040071856 B/op    10158 allocs/op
BenchmarkConcatDirect-4                1        1080713900 ns/op        4040070800 B/op    10147 allocs/op
BenchmarkConcatDirect-4                1        1203622300 ns/op        4040067344 B/op    10111 allocs/op
BenchmarkConcatDirect-4                1        1045291300 ns/op        4040070224 B/op    10141 allocs/op
BenchmarkConcatDirect-4                1        1123163300 ns/op        4040070032 B/op    10139 allocs/op
BenchmarkConcatDirect-8                2         790421300 ns/op        4040076656 B/op    10208 allocs/op
BenchmarkConcatDirect-8                2         659047300 ns/op        4040079488 B/op    10237 allocs/op
BenchmarkConcatDirect-8                2         712991800 ns/op        4040077184 B/op    10213 allocs/op
BenchmarkConcatDirect-8                2         706605350 ns/op        4040078000 B/op    10222 allocs/op
BenchmarkConcatDirect-8                2         656195700 ns/op        4040085248 B/op    10297 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2            2726            386412 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            3439            335358 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            3376            338957 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            3870            326301 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            4285            339596 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1663            671535 ns/op         4128187 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1507            744885 ns/op         4128191 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1353           1097800 ns/op         4128187 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1388           1006019 ns/op         4128189 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1635            993764 ns/op         4128189 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1332            783599 ns/op         4128198 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1818            729821 ns/op         4128202 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1398            780614 ns/op         4128202 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1526            750513 ns/op         4128204 B/op         29 allocs/op
BenchmarkConcatBuilder-8            2164            704798 ns/op         4128204 B/op         29 allocs/op
PASS
ok      golearn/tool_test       50.387s

Em seguida, use benchstat para comparar:

sh
$ benchstat old.txt new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
    old.txt               new.txt
    sec/op    sec/op      vs base
ConcatDirect-2     299.0m ± ¹    286.7m ± ¹        ~ (p=0.310 n=5)
ConcatDirect-4     894.7m ± ¹   1123.2m ± ¹  +25.53% (p=0.008 n=5)
ConcatDirect-8     684.0m ± ¹    706.6m ± ¹        ~ (p=0.548 n=5)
ConcatBuilder-2    423.9µ ± ¹    339.0µ ± ¹  -20.04% (p=0.008 n=5)
ConcatBuilder-4   1068.7µ ± ¹    993.8µ ± ¹        ~ (p=0.151 n=5)
ConcatBuilder-8    736.5µ ± ¹    750.5µ ± ¹        ~ (p=0.841 n=5)
geomean            19.84m          19.65m         -0.98%
¹ need >= 6 samples for confidence interval at level 0.95

    old.txt                new.txt
     B/op     B/op       vs base
ConcatDirect-2    3.763Gi ± ¹   3.763Gi ± ¹       ~ (p=1.000 n=5)
ConcatDirect-4    3.763Gi ± ¹   3.763Gi ± ¹  -0.00% (p=0.008 n=5)
ConcatDirect-8    3.763Gi ± ¹   3.763Gi ± ¹  -0.00% (p=0.008 n=5)
ConcatBuilder-2   3.937Mi ± ¹   3.937Mi ± ¹       ~ (p=1.000 n=5) ²
ConcatBuilder-4   3.937Mi ± ¹   3.937Mi ± ¹       ~ (p=0.079 n=5)
ConcatBuilder-8   3.937Mi ± ¹   3.937Mi ± ¹       ~ (p=0.952 n=5)
geomean           123.2Mi         123.2Mi        -0.00%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal

   old.txt               new.txt
  allocs/op  allocs/op    vs base
ConcatDirect-2    9.999k ± ¹   9.999k ± ¹       ~ (p=1.000 n=5)
ConcatDirect-4    10.22k ± ¹   10.14k ± ¹  -0.74% (p=0.008 n=5)
ConcatDirect-8    10.35k ± ¹   10.22k ± ¹  -1.26% (p=0.008 n=5)
ConcatBuilder-2    29.00 ± ¹    29.00 ± ¹       ~ (p=1.000 n=5) ²
ConcatBuilder-4    29.00 ± ¹    29.00 ± ¹       ~ (p=1.000 n=5) ²
ConcatBuilder-8    29.00 ± ¹    29.00 ± ¹       ~ (p=1.000 n=5) ²
geomean            543.6          541.7        -0.33%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal

Dos resultados, podemos ver que o benchstat os divide em três grupos: tempo de execução, uso de memória e número de alocações de memória. Entre eles, geomean é a média, p é o nível de significância da amostra. A região crítica é normalmente 0.05. Acima de 0.05 não é muito confiável. Pegando um dos dados como exemplo:

          │    sec/op     │    sec/op      vs base               │
ConcatDirect-4     894.7m ± ∞ ¹   1123.2m ± ∞ ¹  +25.53% (p=0.008 n=5)

Podemos ver que o tempo de execução de old é 894.7ms, o tempo de execução de new é 1123.2ms, em comparação, aumentou 25.53% no tempo de execução.

Teste Fuzzy

O teste fuzzy é uma nova funcionalidade lançada no Go 1.18. É uma espécie de aprimoramento dos testes unitários e de benchmark. A diferença é que os dados de teste dos dois anteriores precisam ser escritos manualmente pelo desenvolvedor, enquanto o teste fuzzy pode gerar dados de teste aleatórios através de um corpus. Sobre teste fuzzy em Go, você pode ir para Go Fuzzing para aprender mais conceitos. A vantagem do teste fuzzy é que, em comparação com dados de teste fixos, dados aleatórios podem testar melhor as condições de fronteira do programa. Vamos usar o exemplo do tutorial oficial para explicar. Desta vez, precisamos testar uma função de inverter string. Primeiro, crie o arquivo /tool/strings.go e escreva o seguinte código:

go
package tool

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

Crie o arquivo de teste fuzzy /tool_test/fuzz_tool_test.go e escreva o seguinte código:

go
package tool

import (
  "golearn/tool"
  "testing"
  "unicode/utf8"
)

func FuzzReverse(f *testing.F) {
  testdata := []string{"hello world!", "nice to meet you", "good bye!"}
  for _, data := range testdata {
    f.Add(data)
  }

  f.Fuzz(func(t *testing.T, str string) {
    first := tool.Reverse(str)
    second := tool.Reverse(first)
    t.Logf("str:%q,first:%q,second:%q", str, first, second)
    if str != second {
      t.Errorf("before: %q, after: %q", str, second)
    }
    if utf8.ValidString(str) && !utf8.ValidString(first) {
      t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
    }
  })
}

No teste fuzzy, primeiro precisamos adicionar dados ao corpus de sementes. No exemplo, usamos f.Add() para adicionar, o que ajuda a gerar dados de teste aleatórios subsequentes. Em seguida, usamos f.Fuzz(fn) para testar. A assinatura da função é a seguinte:

go
func (f *F) Fuzz(ff any)

func (f *F) Add(args ...any)

fn é semelhante à lógica de uma função de teste unitário. O primeiro parâmetro de entrada da função deve ser t *testing.T, seguido pelos parâmetros que deseja gerar. Como as strings passadas são imprevisíveis, aqui usamos o método de inverter duas vezes para verificar. Execute o seguinte comando:

sh
$ go test -run Fuzz -v
=== RUN   FuzzReverse
=== RUN   FuzzReverse/seed#0
    fuzz_tool_test.go:18: str:"hello world!",first:"!dlrow olleh",second:"hello world!"
=== RUN   FuzzReverse/seed#1
    fuzz_tool_test.go:18: str:"nice to meet you",first:"uoy teem ot ecin",second:"nice to meet you"
=== RUN   FuzzReverse/seed#2
    fuzz_tool_test.go:18: str:"good bye!",first:"!eyb doog",second:"good bye!"
--- PASS: FuzzReverse (0.00s)
    --- PASS: FuzzReverse/seed#0 (0.00s)
    --- PASS: FuzzReverse/seed#1 (0.00s)
    --- PASS: FuzzReverse/seed#2 (0.00s)
PASS
ok      golearn/tool_test       0.539s

Quando o parâmetro não tem -fuzz, não gerará dados de teste aleatórios. Apenas passará os dados do corpus para a função de teste. Podemos ver dos resultados que todos os testes passaram. Usar desta forma é equivalente a teste unitário, mas na verdade há um problema. Vamos adicionar o parâmetro -fuzz e executar novamente:

sh
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN   FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/217 completed
fuzz: minimizing 91-byte failing input file
fuzz: elapsed: 0s, gathering baseline coverage: 15/217 completed
--- FAIL: FuzzReverse (0.13s)
    --- FAIL: FuzzReverse (0.00s)
        fuzz_tool_test.go:18: str:"𐑄",first:"\x84\x91\x90\xf0",second:"𐑄"
        fuzz_tool_test.go:23: Reverse produced invalid UTF-8 string "𐑄" "\x84\x91\x90\xf0"

    Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
    To re-run:
    go test -run=FuzzReverse/d856c981b6266ba2
=== NAME
FAIL
exit status 1
FAIL    golearn/tool_test       0.697s

TIP

Casos de teste que falham no teste fuzzy serão outputos para um arquivo de corpus no diretório testdata sob a pasta de teste atual, como no exemplo acima:

Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2

testdata\fuzz\FuzzReverse\d856c981b6266ba2 é o caminho do arquivo de corpus outputado. O conteúdo do arquivo é o seguinte:

go test fuzz v1
string("𐑄")

Podemos ver que desta vez não passou. A razão é que a string invertida se tornou um formato não-UTF-8. Portanto, através do teste fuzzy, descobrimos este problema. Como alguns caracteres ocupam mais de um byte, se os invertermos em unidades de byte, certamente se tornarão caracteres ilegíveis. Portanto, modificamos o código fonte a ser testado para o seguinte, convertendo a string para []rune, para evitar o problema acima:

go
func Reverse(s string) string {
    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)
}

Em seguida, execute diretamente o caso de teste que falhou no último teste fuzzy:

sh
$ go test -run=FuzzReverse/d856c981b6266ba2 -v
=== RUN   FuzzReverse
=== RUN   FuzzReverse/d856c981b6266ba2
    fuzz_tool_test.go:18: str:"𐑄",first:"𐑄",second:"𐑄"
--- PASS: FuzzReverse (0.00s)
    --- PASS: FuzzReverse/d856c981b6266ba2 (0.00s)
PASS
ok      golearn/tool_test       0.033s

Podemos ver que desta vez o teste passou. Execute o teste fuzzy novamente para ver se há mais problemas:

sh
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN   FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: minimizing 70-byte failing input file
failure while testing seed corpus entry: FuzzReverse/d97214ce235bfcf5
fuzz: elapsed: 0s, gathering baseline coverage: 2/219 completed
--- FAIL: FuzzReverse (0.15s)
    --- FAIL: FuzzReverse (0.00s)
        fuzz_tool_test.go:18: str:"\xe4",first:"",second:""
        fuzz_tool_test.go:20: before: "\xe4", after: ""

=== NAME
FAIL
exit status 1
FAIL    golearn/tool_test       0.184s

Podemos ver que houve outro erro. Desta vez, o problema é que após inverter a string duas vezes, elas não são iguais. O caractere original é \xe4, o resultado esperado é 4ex\, mas o resultado é caracteres ilegíveis, como segue:

go
func main() {
  fmt.Println("\xe4")
  fmt.Println([]byte("\xe4"))
  fmt.Println([]rune("\xe4"))
  fmt.Printf("%q\n", "\xe4")
  fmt.Printf("%x\n", "\xe4")
}

O resultado da execução é:


[65533]
"\xe4"
e4

A razão é que \xe4 representa um byte, mas não é uma sequência UTF-8 válida (na codificação UTF-8, \xe4 é o início de um caractere de três bytes, mas faltam dois bytes depois). Ao converter para []rune, o Golang automaticamente o transforma em []rune{"\uFFFD"} contendo um único caractere Unicode. Após inverter, ainda é []rune{"\uFFFD"}. Ao converter de volta para string, este caractere Unicode é substituído por sua codificação UTF-8 \xef\xbf\xbd. Portanto, uma solução é que, se a string passada não for UTF-8 válida, retorne diretamente um erro:

go
func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    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
}

O código de teste também precisa ser ligeiramente modificado:

go
func FuzzReverse(f *testing.F) {
  testdata := []string{"hello world!", "nice to meet you", "good bye!"}
  for _, data := range testdata {
    f.Add(data)
  }

  f.Fuzz(func(t *testing.T, str string) {
    first, err := tool.Reverse(str)
    if err != nil {
      t.Skip()
    }
    second, err := tool.Reverse(first)
    if err != nil {
      t.Skip()
    }
    t.Logf("str:%q,first:%q,second:%q", str, first, second)
    if str != second {
      t.Errorf("before: %q, after: %q", str, second)
    }
    if utf8.ValidString(str) && !utf8.ValidString(first) {
      t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
    }
  })
}

Quando a função de inversão retorna error, pula o teste. Em seguida, execute o teste fuzzy:

sh
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN   FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: elapsed: 0s, gathering baseline coverage: 219/219 completed, now fuzzing with 16 workers
fuzz: elapsed: 3s, execs: 895571 (297796/sec), new interesting: 32 (total: 251)
fuzz: elapsed: 6s, execs: 1985543 (363120/sec), new interesting: 37 (total: 256)
fuzz: elapsed: 9s, execs: 3087837 (367225/sec), new interesting: 38 (total: 257)
fuzz: elapsed: 12s, execs: 4090817 (335167/sec), new interesting: 40 (total: 259)
fuzz: elapsed: 15s, execs: 5132580 (346408/sec), new interesting: 44 (total: 263)
fuzz: elapsed: 18s, execs: 6248486 (372185/sec), new interesting: 45 (total: 264)
fuzz: elapsed: 21s, execs: 7366827 (373305/sec), new interesting: 46 (total: 265)
fuzz: elapsed: 24s, execs: 8439803 (358059/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 27s, execs: 9527671 (361408/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 30s, execs: 10569473 (348056/sec), new interesting: 48 (total: 267)
fuzz: elapsed: 30s, execs: 10569473 (0/sec), new interesting: 48 (total: 267)
--- PASS: FuzzReverse (30.16s)
=== NAME
PASS
ok      golearn/tool_test       30.789s

Desta vez, podemos obter um log de output de teste fuzzy mais completo. Algumas explicações de conceitos são as seguintes:

  • elapsed: Tempo decorrido após a conclusão de uma rodada
  • execs: Número total de entradas executadas, 297796/sec significa quantas entradas por segundo
  • new interesting: No teste, o número total de entradas "interessantes" adicionadas ao corpus. (Entradas interessantes referem-se a entradas que podem expandir a cobertura de código para além do escopo que o corpus existente pode cobrir. À medida que a cobertura continua a se expandir, sua tendência de crescimento geralmente continuará a desacelerar)

TIP

Se não houver o parâmetro -fuzztime para limitar o tempo, o teste fuzzy continuará executando para sempre.

Suporte de Tipos

Os tipos suportados no Go Fuzz são os seguintes:

  • string, []byte
  • int, int8, int16, int32/rune, int64
  • uint, uint8/byte, uint16, uint32, uint64
  • float32, float64
  • bool

Golang por www.golangdev.cn edit