Test
Per gli sviluppatori, buoni test possono individuare anticipatamente gli errori nel programma, evitando il carico mentale causato da bug dovuti a manutenzione inadeguata. Pertanto, è molto necessario scrivere buoni test. Go fornisce uno strumento a riga di comando molto semplice e pratico per i test: go test. Nei librerie standard e in molti framework open source si possono vedere tracce di test. Questo strumento è molto comodo da usare e attualmente supporta i seguenti tipi di test:
- Test di esempio
- Test unitari
- Test di benchmark
- Test fuzzy
In Go, la maggior parte delle API sono fornite dalla libreria standard testing.
TIP
Eseguendo il comando go help testfunc nella riga di comando, è possibile vedere la spiegazione ufficiale di Go per i quattro tipi di test sopra menzionati.
Linee Guida per la Scrittura
Prima di iniziare a scrivere i test, è necessario prestare attenzione ad alcune linee guida, in modo da rendere più comodo l'apprendimento successivo.
- Pacchetto di test: i file di test dovrebbero essere preferibilmente collocati in un pacchetto separato, solitamente denominato
test. - File di test: i file di test terminano solitamente con
_test.go. Ad esempio, se si desidera testare una determinata funzione, denominarlafunction_test.go. Se si desidera suddividere ulteriormente in base al tipo di test, è possibile utilizzare il tipo di test come prefisso del file, ad esempiobenchmark_marshaling_test.gooexample_marshaling_test.go. - Funzioni di test: ogni file di test conterrà diverse funzioni di test per diversi tipi di test. Per diversi tipi di test, lo stile di denominazione delle funzioni di test è diverso. Ad esempio, i test di esempio sono
ExampleXXXX, i test unitari sonoTestXXXX, i test di benchmark sonoBenchmarkXXXX, e i test fuzzy sonoFuzzXXXX. In questo modo, anche senza commenti, si può sapere che tipo di test si tratta.
TIP
Quando il nome del pacchetto è testdata, quel pacchetto è solitamente utilizzato per memorizzare dati ausiliari per i test. Durante l'esecuzione dei test, Go ignorerà il pacchetto denominato testdata.
Seguendo le linee guida sopra e adottando uno stile di test di buona qualità, si risparmieranno molti problemi per la manutenzione futura.
Esecuzione dei Test
Per eseguire i test si utilizza principalmente il comando go test. Di seguito viene mostrato un esempio con codice reale. Supponiamo di avere un file da testare /say/hello.go con il seguente codice:
package say
import "fmt"
func Hello() {
fmt.Println("hello")
}
func GoodBye() {
fmt.Println("bye")
}E un file di test /test/example_test.go con il seguente codice:
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
}Ci sono diversi modi per eseguire questi test. Ad esempio, se si desidera eseguire tutti i casi di test nel pacchetto test, è possibile eseguire direttamente il seguente comando nella directory test:
$ go test ./
PASS
ok golearn/test 0.422s./ indica la directory corrente. Go ricompilerà tutti i file di test nella directory test, quindi eseguirà tutti i casi di test. Dal risultato si può vedere che tutti i casi di test sono passati. Il parametro successivo può anche essere seguito da più directory. Ad esempio, il comando seguente, ovviamente, la directory principale del progetto non ha file di test da eseguire:
$ go test ./ ../
ok golearn/test
? golearn [no test files]TIP
Quando i parametri eseguiti sono più pacchetti, Go non eseguirà nuovamente i casi di test già passati con successo. Durante l'esecuzione, verrà aggiunto (cached) alla fine della riga per indicare che il risultato di output è la cache della volta precedente. Quando i flag di test si trovano nel seguente insieme, Go memorizzerà nella cache i risultati del test, altrimenti no:
-benchtime, -cpu,-list, -parallel, -run, -short, -timeout, -failfast, -vSe si desidera disabilitare la cache, è possibile aggiungere il parametro -count=1.
Naturalmente, è anche possibile specificare un singolo file di test da eseguire:
$ go test example_test.go
ok command-line-arguments 0.457sOppure è possibile specificare un singolo caso di test in un file di test. Ad esempio:
$ go test -run ExampleSay
PASS
ok golearn/test 0.038sI tre casi sopra menzionati hanno completato il test, ma il risultato di output è troppo conciso. In questo caso, è possibile aggiungere il parametro -v per rendere l'output più dettagliato. Ad esempio:
$ 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.040sOra è possibile vedere chiaramente l'ordine di esecuzione, il tempo impiegato, la situazione di esecuzione di ogni caso di test e il tempo totale impiegato.
TIP
Il comando go test esegue per impostazione predefinita tutti i test unitari e i test di esempio. Se si aggiunge il parametro -bench, verranno eseguiti tutti i tipi di test. Ad esempio, il comando seguente:
$ go test -bench .Pertanto, è necessario utilizzare il parametro -run per specificare. Ad esempio, il comando per eseguire solo tutti i test di benchmark è il seguente:
$ go test -bench . -run ^$Parametri Comuni
I test di Go hanno molti flag. Di seguito verranno introdotti solo i parametri comunemente utilizzati. Per ulteriori dettagli, si consiglia di utilizzare il comando go help testflag per consultare autonomamente.
| Parametro | Descrizione |
|---|---|
-o file | Specifica il nome del file binario dopo la compilazione |
-c | Compila solo i file di test, ma non li esegue |
-json | Output dei log di test in formato JSON |
-exec xprog | Esegue i test utilizzando xprog, equivalente a go run |
-bench regexp | Seleziona i test di benchmark corrispondenti a regexp |
-fuzz regexp | Seleziona i test fuzzy corrispondenti a regexp |
-fuzztime t | Tempo per terminare automaticamente il test fuzzy, t è l'intervallo di tempo. Quando l'unità è x, indica il numero di volte, ad esempio 200x |
-fuzzminimizetime t | Tempo minimo di esecuzione del test di minimizzazione, regola come sopra |
-count n | Esegue i test n volte, impostazione predefinita 1 volta |
-cover | Attiva l'analisi della copertura dei test |
-covermode set,count,atomic | Imposta la modalità di analisi della copertura |
-cpu | Esegue GOMAXPROCS per i test |
-failfast | Dopo il primo fallimento del test, non verranno avviati nuovi test |
-list regexp | Elenca i casi di test corrispondenti a regexp |
-parallel n | Consente l'esecuzione parallela dei casi di test che hanno chiamato t.Parallel, n è il numero massimo di paralleli |
-run regexp | Esegue solo i casi di test corrispondenti a regexp |
-skip regexp | Salta i casi di test corrispondenti a regexp |
-timeout d | Se un singolo test supera il tempo d, si verificherà un panic. d è un intervallo di tempo, ad esempio 1s, 1ms, 1ns, ecc. |
-shuffle off,on,N | Mescola l'ordine di esecuzione dei test, N è il seme casuale. Il seme predefinito è l'ora di sistema |
-v | Output di log di test più dettagliati |
-benchmem | Statistiche sull'allocazione di memoria per i test di benchmark |
-blockprofile block.out | Statistiche sulla situazione di blocco delle coroutine durante i test e scrittura su file |
-blockprofilerate n | Controlla la frequenza di statistica del blocco delle coroutine. Per ulteriori dettagli, utilizzare il comando go doc runtime.SetBlockProfileRate |
-coverprofile cover.out | Statistiche sulla copertura dei test e scrittura su file |
-cpuprofile cpu.out | Statistiche sulla CPU e scrittura su file |
-memprofile mem.out | Statistiche sull'allocazione della memoria e scrittura su file |
-memprofilerate n | Controlla la frequenza di statistica dell'allocazione della memoria. Per ulteriori dettagli, utilizzare il comando go doc runtime.MemProfileRate |
-mutexprofile mutex.out | Statistiche sulla competizione dei lock e scrittura su file |
-mutexprofilefraction n | Imposta le statistiche su n coroutine che competono per un mutex |
-trace trace.out | Scrittura delle tracce di esecuzione su file |
-outputdir directory | Specifica la directory di output per i file di statistica sopra menzionati. L'impostazione predefinita è la directory di esecuzione di go test |
Test di Esempio
I test di esempio non servono a scoprire problemi nel programma come gli altri tre tipi di test. Servono piuttosto a dimostrare l'utilizzo di una determinata funzione, svolgendo un ruolo di documentazione. I test di esempio non sono un concetto definito ufficialmente, né una norma rigida. Sono più una convenzione ingegneristica. Se遵守 o meno dipende dallo sviluppatore. I test di esempio appaiono molto frequentemente nelle librerie standard. Di solito sono esempi di codice delle librerie standard scritti ufficialmente. Ad esempio, la funzione di test ExampleWithDeadline in context/example_test.go della libreria standard mostra l'utilizzo di base di 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
}A prima vista, questa funzione di test è solo una funzione ordinaria. Tuttavia, i test di esempio sono principalmente evidenziati dal commento Output. Quando la funzione da testare ha solo una riga di output, si utilizza il commento Output per verificare l'output. Innanzitutto, crea un file denominato hello.go e scrivi il seguente codice:
package say
import "fmt"
func Hello() {
fmt.Println("hello")
}
func GoodBye() {
fmt.Println("bye")
}La funzione SayHello è la funzione da testare. Poi crea un file di test example_test.go e scrivi il seguente codice:
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
}Il commento Output nella funzione indica che verifica se l'output della funzione è hello. Successivamente, esegui il comando di test per vedere il risultato:
$ 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.448sDal risultato si può vedere che tutti i test sono passati. Per quanto riguarda Output, ci sono diversi modi di scrittura. Il primo è con una sola riga di output, che significa verificare se l'output della funzione è hello:
// Output:
// helloIl secondo è con output su più righe, ovvero verifica se l'output corrisponde in ordine:
// Output:
// hello
// byeIl terzo è output non ordinato, ovvero output su più righe senza seguire l'ordine:
// Unordered output:
// bye
// helloÈ importante notare che, per le funzioni di test, solo quando le ultime righe sono commenti Output saranno considerate test di esempio. Altrimenti, sono solo funzioni ordinarie e non verranno eseguite da Go.
Test Unitari
I test unitari testano le unità testabili più piccole nel software. La definizione della dimensione dell'unità dipende dallo sviluppatore. Potrebbe essere una struttura, un pacchetto, una funzione o un tipo. Di seguito viene dimostrato ancora tramite un esempio. Innanzitutto, crea il file /tool/math.go e scrivi il seguente codice:
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
}Poi crea il file di test /tool_test/unit_test.go. Per i test unitari, la denominazione può essere unit_test oppure utilizzare il pacchetto o la funzione da testare come prefisso del file:
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)
}
}Per i test unitari, lo stile di denominazione di ogni caso di test è TestXXXX e il parametro di ingresso della funzione deve essere t *testing.T. testing.T è una struttura fornita dal pacchetto testing per facilitare i test e offre molti metodi disponibili. t.Errorf nell'esempio è equivalente a t.Logf e viene utilizzato per output formattato di informazioni di log quando il test fallisce. Altri comunemente utilizzati includono t.Fail per contrassegnare il caso corrente come fallito. Funzioni simili includono t.FailNow, che contrassegna anche come fallito, ma il primo continua l'esecuzione dopo il fallimento, mentre il secondo si interrompe direttamente. Nell'esempio seguente, modifica il risultato previsto in un risultato errato:
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 utilizza 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 utilizza internamente t.FailNow()
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
t.Log("test finished")
}Eseguendo il test sopra, l'output è il seguente:
$ 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.037sDal log di test si può vedere che TestSum ha output "test finished" anche se è fallito, mentre TestEqual no. Allo stesso modo, t.SkipNow contrassegna il caso corrente come SKIP e interrompe l'esecuzione. Verrà eseguito nuovamente nel prossimo round di test:
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")
}Durante l'esecuzione del test, modifica il numero di test a 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.468sNegli esempi sopra, "test finished" viene output nell'ultima riga per indicare il completamento del test. In realtà, è possibile utilizzare t.Cleanup per registrare una funzione di cleanup dedicata a questo scopo. Questa funzione verrà eseguita alla fine del caso di test, come 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)
}
}Dopo aver eseguito il test, l'output è il seguente:
$ 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
Tramite t.Helper(), è possibile contrassegnare la funzione corrente come funzione helper. Le funzioni helper non vengono eseguite come casi di test separati. Quando si registrano i log, il numero di riga output è anche il numero di riga del chiamante della funzione helper. Questo rende il posizionamento più accurato durante l'analisi dei log, evitando informazioni ridondanti. Ad esempio, l'esempio t.Cleanup sopra può essere modificato in una funzione helper, come 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)
}
}Dopo aver eseguito il test, le informazioni di output sono le seguenti. La differenza rispetto a prima è che il numero di riga di "test finished" diventa il numero di riga del chiamante:
$ 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
Le operazioni sopra possono essere eseguite solo nei test principali, ovvero nei casi di test eseguiti direttamente. Se utilizzate in un sottotest, si verificherà panic.
Sottotest
In alcuni casi, potrebbe essere necessario testare altri casi di test all'interno di un caso di test. Questi casi di test annidati sono generalmente chiamati sottotest. Tramite il metodo t.Run(), la cui firma è la seguente:
// Il metodo Run avvia una nuova coroutine per eseguire il sottotest e blocca l'attesa del completamento della funzione f prima di tornare
// Il valore di ritorno indica se il test è passato
func (t *T) Run(name string, f func(t *T)) boolDi seguito è riportato un esempio:
func TestTool(t *testing.T) {
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
}Dopo l'esecuzione, il risultato è il seguente:
$ 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.449sDall'output si può vedere chiaramente la struttura gerarchica padre-figlio. Nell'esempio sopra, il secondo sottotest non verrà eseguito fino al completamento del primo. È possibile utilizzare t.Parallel() per contrassegnare i casi di test come eseguibili in parallelo. In questo modo, l'ordine di output non sarà determinabile.
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")
}Dopo aver eseguito il test, l'output è il seguente:
$ 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.444sDal risultato del test si può vedere chiaramente che c'è un processo di attesa di blocco. Durante l'esecuzione concorrente dei casi di test, come nell'esempio sopra, sicuramente non può procedere normalmente, poiché il codice successivo non può garantire l'esecuzione sincrona. In questo caso, è possibile scegliere di annidare un altro livello di t.Run(), come 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")
}Eseguendo nuovamente, è possibile vedere il risultato di esecuzione normale:
$ 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.450sStile a Tabella
Nei test unitari sopra, i dati di input dei test sono variabili dichiarate manualmente. Quando la quantità di dati è piccola, non è un problema. Ma se si desidera testare più gruppi di dati, non è possibile dichiarare variabili per creare dati di test ogni volta. Pertanto, in generale, si cerca di utilizzare il più possibile slice di strutture. La struttura è una struttura anonima dichiarata temporaneamente. Poiché questo stile di codifica sembra una tabella, è chiamato table-driven. Di seguito è riportato un esempio. Questo è un esempio in cui si dichiarano manualmente più variabili per creare dati di test. Se ci sono più gruppi di dati, non è molto intuitivo. Quindi viene modificato in stile a tabella:
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)
}
}Il codice modificato è il seguente:
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)
}
}
}Questi dati di test sembrano molto più intuitivi.
Test di Benchmark
I test di benchmark, noti anche come test delle prestazioni, sono solitamente utilizzati per testare indicatori di prestazioni come l'occupazione della memoria, l'utilizzo della CPU, il tempo di esecuzione, ecc. Per i test di benchmark, i file di test terminano solitamente con bench_test.go, e il nome della funzione del caso di test deve essere nel formato BenchmarkXXXX.
Di seguito viene mostrato un esempio di confronto delle prestazioni di concatenazione di stringhe. Innanzitutto, crea il file /tool/strConcat.go. Come è noto, l'utilizzo diretto della concatenazione di stringhe con + ha prestazioni molto basse, mentre l'utilizzo di strings.Builder è molto migliore. Nel file /tool/strings.go, crea due funzioni per eseguire due tipi di concatenazione di stringhe:
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)
}
}Poi crea il file di test /tool_test/bench_tool_test.go con il seguente codice:
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)
}
}Esegui il comando di test. Il comando attiva i log dettagliati e l'analisi della memoria, specifica l'elenco dei nuclei CPU utilizzati e ogni caso di test viene eseguito due volte. L'output è il seguente:
$ 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.381sDi seguito viene spiegata l'interpretazione del risultato di output del test di benchmark. goos rappresenta il sistema operativo in esecuzione, goarh rappresenta l'architettura della CPU, pkg è il pacchetto in cui si trova il test, cpu sono alcune informazioni sulla CPU. Il risultato di ogni caso di test è separato dal nome di ogni test di benchmark. La prima colonna BenchmarkConcatDirect-2, il 2 rappresenta il numero di nuclei CPU utilizzati. La seconda colonna 4 rappresenta la dimensione di b.N nel codice, ovvero il numero di cicli nel test di benchmark. La terza colonna 277771375 ns/op rappresenta il tempo consumato per ogni ciclo, ns è nanosecondi. La quarta colonna 4040056736 B/op rappresenta la dimensione in byte della memoria allocata per ogni ciclo. La quinta colonna 10000 allocs/op rappresenta il numero di allocazioni di memoria per ogni ciclo.
Ovviamente, in base ai risultati del test, le prestazioni dell'utilizzo di strings.Builder sono molto superiori all'utilizzo di + per concatenare stringhe. Il confronto intuitivo dei dati delle prestazioni è proprio lo scopo dei test di benchmark.
benchstat
benchstat è uno strumento open source per l'analisi dei test delle prestazioni. Il campione dei test delle prestazioni sopra ha solo due gruppi. Una volta che i campioni aumentano, l'analisi manuale diventa molto dispendiosa in termini di tempo e fatica. Questo strumento è nato per risolvere i problemi di analisi delle prestazioni.
Innanzitutto, scarica lo strumento:
$ go install golang.org/x/perf/benchstatEsegui i test di benchmark due volte. Questa volta modifica il numero di campioni a 5 e output rispettivamente nei file old.txt e new.txt per il confronto. Primo risultato di esecuzione:
$ 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.742sSecondo risultato di esecuzione:
$ 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.387sPoi utilizza benchstat per il confronto:
$ 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 equalDal risultato si può vedere che benchstat li ha divisi in tre gruppi: tempo di esecuzione, occupazione della memoria e numero di allocazioni di memoria. Tra questi, geomean è il valore medio, p è il livello di significatività del campione. L'intervallo critico è solitamente 0.05. Se superiore a 0.05, non è molto affidabile. Prendi uno dei dati come esempio:
│ sec/op │ sec/op vs base │
ConcatDirect-4 894.7m ± ∞ ¹ 1123.2m ± ∞ ¹ +25.53% (p=0.008 n=5)Come si può vedere, il tempo di esecuzione di old è 894.7ms, il tempo di esecuzione di new è 1123.2ms. Rispetto a questo, è aumentato del 25.53%.
Test Fuzzy
Il test fuzzy è una nuova funzionalità introdotta in GO1.18. È un potenziamento dei test unitari e dei test di benchmark. La differenza è che i dati di test dei primi due devono essere scritti manualmente dallo sviluppatore, mentre i test fuzzy possono generare dati di test casuali tramite un corpus. Per i concetti di test fuzzy in Go, puoi andare su Go Fuzzing per ulteriori informazioni. Il vantaggio dei test fuzzy è che, rispetto ai dati di test fissi, i dati casuali possono testare meglio le condizioni al contorno del programma. Di seguito viene spiegato utilizzando l'esempio del tutorial ufficiale. Questa volta la funzione da testare è una funzione di inversione di stringhe. Innanzitutto, crea il file /tool/strings.go e scrivi il seguente codice:
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)
}Crea il file di test fuzzy /tool_test/fuzz_tool_test.go e scrivi il seguente codice:
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)
}
})
}Nei test fuzzy, è necessario prima aggiungere dati al seed corpus. Nell'esempio viene utilizzato f.Add() per aggiungere, il che aiuta a generare dati di test casuali successivi. Poi si utilizza f.Fuzz(fn) per eseguire il test. La firma della funzione è la seguente:
func (f *F) Fuzz(ff any)
func (f *F) Add(args ...any)fn è simile alla logica di una funzione di test unitario. Il primo parametro di ingresso deve essere t *testing.T, seguito dai parametri che si desidera generare. Poiché la stringa passata è imprevedibile, qui si utilizza il metodo di inversione due volte per la verifica. Esegui il seguente 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 il parametro non include -fuzz, non verranno generati dati di test casuali. Verranno passati solo i dati del corpus alla funzione di test. Dal risultato si può vedere che tutti i test sono passati. Utilizzandolo in questo modo, è equivalente a un test unitario. Ma in realtà c'è un problema. Di seguito, aggiungi il parametro -fuzz ed esegui nuovamente:
$ 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
I casi di test falliti nei test fuzzy verranno output in un file corpus nella directory testdata sotto la cartella di test corrente. Ad esempio, nell'esempio sopra:
Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2testdata\fuzz\FuzzReverse\d856c981b6266ba2 è il percorso del file corpus output. Il contenuto del file è il seguente:
go test fuzz v1
string("𐑄")Come si può vedere, questa volta non è passato. Il motivo è che la stringa invertita è diventata un formato non UTF-8. Quindi, attraverso il test fuzzy, è stato scoperto questo problema. Poiché alcuni caratteri occupano più di un byte, se vengono invertiti in unità di byte, sicuramente diventeranno codici casuali. Quindi modifica il codice sorgente da testare come segue, convertendo la stringa in []rune. In questo modo, è possibile evitare il problema sopra:
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)
}Successivamente, esegui direttamente il caso di test fallito del test fuzzy precedente:
$ 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.033sCome si può vedere, questa volta il test è passato. Esegui nuovamente il test fuzzy per vedere se ci sono altri problemi:
$ 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.184sCome si può vedere, c'è di nuovo un errore. Questa volta il problema è che dopo due inversioni della stringa, non sono uguali. Il carattere originale è \xe4, il risultato atteso è 4ex\, ma il risultato è codice casuale. Di seguito:
func main() {
fmt.Println("\xe4")
fmt.Println([]byte("\xe4"))
fmt.Println([]rune("\xe4"))
fmt.Printf("%q\n", "\xe4")
fmt.Printf("%x\n", "\xe4")
}Il risultato di esecuzione è:
[65533]
"\xe4"
e4La ragione è che \xe4 rappresenta un byte, ma non è una sequenza UTF-8 valida (nella codifica UTF-8, \xe4 è l'inizio di un carattere a tre byte, ma mancano due byte successivi). Quando viene convertito in []rune, Go lo trasforma automaticamente in []rune{"\uFFFD"} contenente un singolo carattere Unicode. Dopo l'inversione, rimane []rune{"\uFFFD"}. Quando viene riconvertito in string, questo carattere Unicode viene sostituito con la sua codifica UTF-8 \xef\xbf\xbd. Pertanto, una soluzione è restituire direttamente un errore se la stringa passata non è UTF-8:
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
}Il codice di test deve anche essere leggermente modificato:
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 la funzione di inversione restituisce error, salta il test. Poi esegui nuovamente il test 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.789sPoi questa volta è possibile ottenere un log di output di test fuzzy più completo. Di seguito è riportata la spiegazione di alcuni concetti:
- elapsed: tempo trascorso dopo il completamento di un round
- execs: numero totale di input eseguiti. 297796/sec indica quanti input al secondo
- new interesting: nei test, il numero totale di input "interessanti" aggiunti al corpus. (Un input interessante si riferisce a un input che può espandere la copertura del codice oltre l'intervallo coperto dal corpus esistente. Con l'espansione continua dell'intervallo di copertura, la sua tendenza di crescita generalmente rallenterà continuamente)
TIP
Se non c'è il parametro -fuzztime per limitare il tempo, il test fuzzy continuerà a essere eseguito per sempre.
Supporto dei Tipi
I tipi supportati in Go Fuzz sono i seguenti:
string,[]byteint,int8,int16,int32/rune,int64uint,uint8/byte,uint16,uint32,uint64float32,float64bool
