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 comofunction_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, comobenchmark_marshaling_test.goouexample_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:
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:
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:
$ 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:
$ 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, -vSe quiser desativar o cache, pode adicionar o parâmetro -count=1.
Claro, também é possível especificar um determinado arquivo de teste para executar:
$ go test example_test.go
ok command-line-arguments 0.457sOu pode especificar um determinado caso de teste em um determinado arquivo de teste, por exemplo:
$ go test -run ExampleSay
PASS
ok golearn/test 0.038sEmbora 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:
$ 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.040sAgora 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:
$ 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 é:
$ 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âmetro | Descrição |
|---|---|
-o file | Especifica o nome do arquivo binário após compilação |
-c | Compila apenas arquivos de teste, mas não executa |
-json | Output de logs de teste em formato json |
-exec xprog | Executa testes usando xprog, equivalente a go run |
-bench regexp | Seleciona testes de benchmark que correspondem a regexp |
-fuzz regexp | Seleciona testes fuzzy que correspondem a regexp |
-fuzztime t | Tempo 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 t | Tempo mínimo de execução do teste de minimização, mesma regra acima |
-count n | Executa testes n vezes, padrão é 1 vez |
-cover | Ativa análise de cobertura de teste |
-covermode set,count,atomic | Define o modo de análise de cobertura |
-cpu | Executa GOMAXPROCS para testes |
-failfast | Após a primeira falha de teste, não inicia novos testes |
-list regexp | Lista casos de teste que correspondem a regexp |
-parallel n | Permite que casos de teste que chamaram t.Parallel sejam executados em paralelo, n é o número máximo de paralelismo |
-run regexp | Executa apenas casos de teste que correspondem a regexp |
-skip regexp | Pula casos de teste que correspondem a regexp |
-timeout d | Se 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,N | Embaralha a ordem de execução dos testes, N é a semente aleatória, a semente padrão é o tempo do sistema |
-v | Output de logs de teste mais detalhados |
-benchmem | Estatísticas de alocação de memória para testes de benchmark |
-blockprofile block.out | Estatísticas de bloqueio de goroutines durante testes e grava em arquivo |
-blockprofilerate n | Controla a frequência de estatísticas de bloqueio de goroutines. Veja mais detalhes através do comando go doc runtime.SetBlockProfileRate |
-coverprofile cover.out | Estatísticas de cobertura de teste e grava em arquivo |
-cpuprofile cpu.out | Estatísticas de CPU e grava em arquivo |
-memprofile mem.out | Estatísticas de alocação de memória e grava em arquivo |
-memprofilerate n | Controla 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.out | Estatísticas de contenção de lock e grava em arquivo |
-mutexprofilefraction n | Define estatísticas de n goroutines competindo por um mutex |
-trace trace.out | Grava o rastreamento de execução em arquivo |
-outputdir directory | Especifica 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:
// 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:
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:
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:
$ 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.448sDos 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:
// helloA segunda é ter múltiplas linhas de saída, ou seja, detectar se a saída corresponde em ordem:
// Output:
// hello
// byeA terceira é saída não ordenada, ou seja, múltiplas linhas de saída sem seguir a ordem:
// Unordered output:
// bye
// helloVale 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:
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:
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:
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:
$ 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.037sDo 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.
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:
$ 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.468sNo 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:
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:
$ 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.462sHelper
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:
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:
$ 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.464sTIP
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:
// 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)) boolAqui está um exemplo:
func TestTool(t *testing.T) {
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
}Executar produz o seguinte resultado:
$ 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.449sAtravé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:
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:
$ 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.444sDo 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:
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:
$ 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.450sEstilo 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:
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:
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:
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:
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:
$ 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.381sAbaixo 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:
$ go install golang.org/x/perf/benchstatExecute 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:
$ 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.742sO resultado da segunda execução:
$ 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.387sEm seguida, use benchstat para comparar:
$ 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 equalDos 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:
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:
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:
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:
$ 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.539sQuando 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:
$ 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.697sTIP
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/d856c981b6266ba2testdata\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:
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:
$ 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.033sPodemos ver que desta vez o teste passou. Execute o teste fuzzy novamente para ver se há mais problemas:
$ 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.184sPodemos 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:
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"
e4A 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:
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:
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:
$ 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.789sDesta 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,[]byteint,int8,int16,int32/rune,int64uint,uint8/byte,uint16,uint32,uint64float32,float64bool
