Il codice da testare
Gli strumenti di test di Go fanno parte della toolchain standard: il pacchetto testing e il comando go test. Nessun framework da installare, nessuna libreria di asserzioni necessaria.
Gli esempi di questa pagina testano una piccola funzione, Slugify, che trasforma un titolo in uno slug per URL. Eccola, eseguibile, con un controllo fatto a mano in main:
L'editor nel browser esegue main; non può eseguire go test. Tutto quello che segue è scritto come si trova in un progetto reale, con l'output del terminale mostrato dopo ogni comando. Tutto è stato eseguito con Go 1.24.
Il primo test
In un progetto la funzione vive in un pacchetto, e i suoi test stanno accanto in un file il cui nome termina con _test.go:
textutil/
├── go.mod module example.com/textutil
├── slug.go package textutil, func Slugify
└── slug_test.go package textutil, the tests
// slug_test.go
package textutil
import "testing"
func TestSlugify(t *testing.T) {
got := Slugify("Hello, World!")
want := "hello-world"
if got != want {
t.Errorf("Slugify(%q) = %q, want %q", "Hello, World!", got, want)
}
}
Le regole che usa go test:
- Solo i file che terminano con
_test.gosono file di test.go buildli ignora, quindi il codice di test non finisce mai nel tuo binario. - Un test è una funzione chiamata
TestXxx(la parte dopoTestnon deve iniziare con una lettera minuscola) che accetta un*testing.T. - Un test passa a meno che non chiami uno dei metodi di fallimento o vada in panic.
go test # the package in the current directory
go test ./... # every package in the module
$ go test
PASS
ok example.com/textutil 0.318s
Il formato del messaggio di errore Func(input) = got, want expected è la convenzione di Go. Metti l'input nel messaggio: un fallimento che dice solo got "a", want "b" ti costringe a tornare nel codice per capire quale fosse l'input.
t.Errorf e t.Fatalf
| Metodo | Segna come fallito | Ferma il test |
|---|---|---|
t.Error, t.Errorf | sì | no, continua |
t.Fatal, t.Fatalf | sì | sì, subito |
t.Log, t.Logf | no | no, stampa solo con -v o in caso di fallimento |
t.Skip, t.Skipf | no | sì, segnalato come saltato |
Usa Errorf come scelta predefinita, così una sola esecuzione segnala ogni campo sbagliato. Usa Fatalf quando il resto del test non può funzionare, di solito dopo un errore inatteso:
func TestCountWords(t *testing.T) {
path := writeFile(t, "doc.txt", "the quick brown\nfox jumps\n")
got, err := CountWords(path)
if err != nil {
t.Fatalf("CountWords: unexpected error: %v", err) // stop: got is meaningless
}
if got != 5 {
t.Errorf("CountWords = %d, want 5", got)
}
}
Fatal ferma il test chiamando runtime.Goexit, quindi va chiamato dalla goroutine del test stesso. Da una goroutine che hai avviato tu, segnala con t.Error e ritorna.
Test a tabella con t.Run
La maggior parte dei test Go sono tabelle: uno slice di casi, un ciclo e t.Run per dare a ogni caso un nome.
func TestSlugifyTable(t *testing.T) {
tests := []struct {
name, in, want string
}{
{"simple", "Go Testing", "go-testing"},
{"punctuation", "What's new in Go 1.24?", "what-s-new-in-go-1-24"},
{"extra spaces", " lots of space ", "lots-of-space"},
{"non-ascii dropped", "Café au lait", "caf-au-lait"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Slugify(tt.in); got != tt.want {
t.Errorf("Slugify(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
Aggiungere un caso richiede una riga. Ogni subtest viene riportato separatamente, un Fatal dentro uno di essi termina solo quel subtest, e puoi eseguirne uno singolo per nome. Esegui tutto con -v:
$ go test -v
=== RUN TestSlugify
--- PASS: TestSlugify (0.00s)
=== RUN TestSlugifyTable
=== RUN TestSlugifyTable/simple
=== RUN TestSlugifyTable/punctuation
=== RUN TestSlugifyTable/extra_spaces
=== RUN TestSlugifyTable/non-ascii_dropped
=== RUN TestSlugifyTable/empty
--- PASS: TestSlugifyTable (0.00s)
--- PASS: TestSlugifyTable/simple (0.00s)
--- PASS: TestSlugifyTable/punctuation (0.00s)
--- PASS: TestSlugifyTable/extra_spaces (0.00s)
--- PASS: TestSlugifyTable/non-ascii_dropped (0.00s)
--- PASS: TestSlugifyTable/empty (0.00s)
=== RUN TestCountWords
--- PASS: TestCountWords (0.00s)
=== RUN TestCountWordsMissingFile
--- PASS: TestCountWordsMissingFile (0.00s)
=== RUN ExampleSlugify
--- PASS: ExampleSlugify (0.00s)
PASS
ok example.com/textutil 0.182s
Gli spazi nei nomi dei subtest diventano underscore. Quando un caso fallisce, l'output indica il subtest e la riga:
--- FAIL: TestSlugifyTable (0.00s)
--- FAIL: TestSlugifyTable/underscore (0.00s)
slug_test.go:27: Slugify("snake_case_name") = "snake-case-name", want "snake_case_name"
FAIL
FAIL example.com/textutil 0.289s
Da Go 1.22 ogni iterazione del ciclo ha il suo tt, quindi le closure dentro t.Run vedono il caso giusto anche quando i subtest girano in parallelo. Il codice più vecchio ha spesso tt := tt all'inizio del corpo del ciclo proprio per questo motivo; ora non serve più.
Per eseguire i subtest in parallelo, chiama t.Parallel() all'inizio della funzione del subtest. Fallo solo quando i casi sono indipendenti e abbastanza lenti da farne valere la pena.
I flag di go test che userai
| Comando | Cosa fa |
|---|---|
go test -v | stampa il nome, il risultato e l'output di t.Log di ogni test |
go test -run TestSlugify | esegue i test il cui nome corrisponde all'espressione regolare |
go test -run 'TestSlugifyTable/empty' | esegue un singolo subtest |
go test -count=1 | ignora i risultati in cache ed esegue di nuovo |
go test -race | esegue con il rilevatore di data race |
go test -short | chiede ai test lunghi di saltarsi da soli (if testing.Short() { t.Skip() }) |
go test -failfast | si ferma dopo il primo test fallito |
go test -timeout 30s | fallisce se l'esecuzione dura di più (default 10 minuti) |
go test -cover | stampa la copertura delle istruzioni |
go test -bench=. | esegue anche i benchmark |
Quando indichi dei pacchetti (go test ., go test ./...), go test mette in cache i risultati dei pacchetti il cui codice e i cui input non sono cambiati e stampa (cached) accanto a essi. Un semplice go test senza argomenti non usa mai la cache. La cache tiene traccia dei file e delle variabili d'ambiente che un test legge tramite il pacchetto os, ma non dello stato esterno come un database o un servizio di rete, quindi un test che dipende da uno di questi può passare dalla cache quando ora fallirebbe; -count=1 forza un'esecuzione reale.
Helper, cartelle temporanee e pulizia
La chiamata writeFile in TestCountWords qui sopra è un helper di test:
// writeFile is a test helper: t.Helper makes failures point at the caller.
func writeFile(t *testing.T, name, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name) // removed automatically after the test
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("writing %s: %v", name, err)
}
return path
}
t.Helper()segna la funzione come helper, così un fallimento viene riportato alla riga del test che l'ha chiamata, non dentro l'helper.t.TempDir()crea una cartella nuova e la elimina quando il test termina. I test non devono mai ripulire i file da soli.t.Cleanup(func() { ... })registra qualsiasi altra operazione di chiusura (chiudere un server, eliminare una tabella). Le funzioni di pulizia vengono eseguite dopo il test e i suoi subtest, a partire dall'ultima registrata.t.Setenv("KEY", "value")imposta una variabile d'ambiente solo per questo test e la ripristina alla fine.t.Context()(Go 1.24) restituisce un context che viene cancellato subito prima dell'esecuzione delle funzioni di pulizia.
I file di fixture per i test vanno in una cartella chiamata testdata accanto ai test. Lo strumento go la ignora come pacchetto, e i test girano con la cartella del pacchetto come directory di lavoro, quindi os.ReadFile("testdata/input.json") funziona.
Testare errori e handler HTTP
Controlla gli errori con errors.Is o errors.As, come farebbe il codice chiamante:
func TestCountWordsMissingFile(t *testing.T) {
_, err := CountWords(filepath.Join(t.TempDir(), "nope.txt"))
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("err = %v, want fs.ErrNotExist", err)
}
}
Confrontare le stringhe degli errori si rompe non appena qualcuno riformula un messaggio.
Per gli handler HTTP, net/http/httptest ti fornisce un finto ResponseWriter così puoi chiamare direttamente l'handler, senza rete:
func TestHealth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
if body := rec.Body.String(); body != "ok\n" {
t.Errorf("body = %q, want %q", body, "ok\n")
}
}
httptest.NewServer avvia un server reale su localhost per testare il codice client. La pagina sul client HTTP lo usa in ogni esempio.
Copertura
$ go test -cover
PASS
coverage: 100.0% of statements
ok example.com/textutil 0.578s
$ go test -coverprofile=cover.out
$ go tool cover -func=cover.out
example.com/textutil/slug.go:11: Slugify 100.0%
example.com/textutil/words.go:9: CountWords 100.0%
total: (statements) 100.0%
$ go tool cover -html=cover.out # opens a browser with covered lines in green
La copertura ti dice quali righe non sono mai state eseguite, ed è utile per trovare rami non testati. Un numero alto non ti dice se le asserzioni sono buone: un test che chiama ogni funzione senza controllare nulla arriva al 100%.
Benchmark
Un benchmark è func BenchmarkXxx(b *testing.B). Go 1.24 ha aggiunto b.Loop, che ora è la forma consigliata:
func BenchmarkSlugify(b *testing.B) {
for b.Loop() {
Slugify("The Go Programming Language, 2nd Edition")
}
}
b.Loop esegue il corpo tutte le volte necessarie per una misura stabile, esclude dalla misurazione il codice di preparazione prima del ciclo e impedisce al compilatore di eliminare la chiamata con le ottimizzazioni. Il codice scritto prima di Go 1.24 usa for i := 0; i < b.N; i++, che funziona ancora ma richiede b.ResetTimer() dopo una preparazione costosa e può essere ingannato dall'eliminazione del codice morto.
I benchmark non vengono eseguiti con un semplice go test. Devi richiederli, e salta i test normali con -run='^$':
$ go test -bench=. -benchmem -run='^$'
goos: darwin
goarch: arm64
pkg: example.com/textutil
cpu: Apple M4
BenchmarkSlugify-10 5061945 238.1 ns/op 168 B/op 5 allocs/op
PASS
ok example.com/textutil 1.410s
Le colonne sono: il nome con GOMAXPROCS aggiunto in coda, le iterazioni eseguite, il tempo per chiamata, i byte allocati per chiamata, le allocazioni per chiamata. I numeri dipendono dalla macchina; confronta esecuzioni fatte sulla stessa. Per confrontare in modo affidabile prima e dopo una modifica, esegui ogni versione più volte con -count=10 e passa entrambi gli output a benchstat (golang.org/x/perf/cmd/benchstat).
Anche gli esempi sono test
Una funzione di esempio stampa qualcosa e dichiara l'output atteso in un commento. go test la esegue e fallisce se l'output è diverso, e go doc e pkg.go.dev la mostrano come documentazione:
// example_test.go
package textutil_test
import (
"fmt"
"example.com/textutil"
)
func ExampleSlugify() {
fmt.Println(textutil.Slugify("Hello, World!"))
// Output: hello-world
}
Il nome del pacchetto textutil_test rende questo un test esterno: può usare solo l'API esportata, come farebbe un vero chiamante. File di questo tipo possono stare nella stessa cartella del pacchetto. Senza un commento // Output: l'esempio viene compilato ma non eseguito. Usa // Unordered output: quando le righe possono comparire in qualsiasi ordine.
Fuzzing, in breve
Go 1.18 ha aggiunto i fuzz test, che generano input per trovare crash e invarianti violate:
func FuzzSlugify(f *testing.F) {
f.Add("Hello, World!") // seed input
f.Fuzz(func(t *testing.T, s string) {
slug := Slugify(s)
if strings.Contains(slug, "--") || strings.HasPrefix(slug, "-") {
t.Errorf("Slugify(%q) = %q: bad hyphens", s, slug)
}
})
}
Un semplice go test esegue solo gli input seme (i valori di f.Add ed eventuali file salvati in testdata/fuzz). go test -fuzz=FuzzSlugify continua a generarne di nuovi finché non trova un fallimento o finché non lo fermi, e salva gli input che falliscono in testdata/fuzz così diventano normali casi di test.
Errori comuni
- File di test nel posto sbagliato o con il nome sbagliato. Deve terminare con
_test.goe stare nella cartella del pacchetto.slug_tests.goviene compilato nel pacchetto, non eseguito come test. - Minuscola dopo
Test.Testslugifynon è un test.TestSlugifyeTest_slugifysì. t.Fatalda un'altra goroutine. Lì segnala cont.Error, e aspetta la goroutine prima che il test ritorni.- Messaggi senza l'input. Includi cosa è stato passato, cosa è uscito e cosa ci si aspettava.
- Fidarsi di un successo in cache. Usa
-count=1quando un test dipende da qualcosa fuori dal pacchetto. - Test che dipendono dall'ordine degli altri o da variabili globali condivise. Ogni test dovrebbe preparare il proprio stato, ed è a questo che servono
t.TempDir,t.Setenvet.Cleanup.
Domande frequenti
Come scrivo un unit test in Go?
Crea un file che termina con _test.go nella stessa cartella del codice, importa testing e scrivi una funzione func TestName(t *testing.T) che chiama il tuo codice e segnala le differenze con t.Errorf. Esegui go test in quella cartella, oppure go test ./... per l'intero modulo. Non servono framework né librerie di asserzioni.
Qual è la differenza tra t.Error e t.Fatal in Go?
t.Error e t.Errorf segnano il test come fallito e continuano a eseguirlo, così una sola esecuzione può segnalare più problemi. t.Fatal e t.Fatalf lo segnano come fallito e fermano subito il test. Usa Fatal quando continuare non ha senso, per esempio dopo un errore inatteso che rende inutilizzabile il risultato.
Come eseguo un singolo test in Go?
Passa un'espressione regolare a -run: go test -run TestSlugify esegue ogni test il cui nome corrisponde, e go test -run 'TestSlugifyTable/punctuation' esegue un solo subtest (gli spazi nei nomi dei subtest diventano underscore). Aggiungi -v per vedere il nome e il risultato di ogni test, e -count=1 per saltare la cache dei test.
Come scrivo un benchmark in Go?
Scrivi func BenchmarkName(b *testing.B) in un file _test.go e metti il codice da misurare in un ciclo for b.Loop() { ... } (Go 1.24; il codice più vecchio usa for i := 0; i < b.N; i++). Eseguilo con go test -bench=. -benchmem, che riporta nanosecondi, byte e allocazioni per operazione.
Come vedo la copertura dei test in Go?
go test -cover stampa la percentuale di istruzioni eseguite dai test. Per i dettagli, scrivi un profilo con go test -coverprofile=cover.out, poi esegui go tool cover -func=cover.out per i numeri per funzione oppure go tool cover -html=cover.out per vedere in un browser le righe coperte e quelle non coperte.