Menu

Test in Golang: unit test, test a tabella e benchmark

Come testare il codice Go con il pacchetto standard testing e go test: file _test.go, funzioni TestXxx, t.Errorf e t.Fatalf, test a tabella con t.Run, helper e cartelle temporanee, copertura, benchmark con b.Loop e test di esempio.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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.go sono file di test. go build li ignora, quindi il codice di test non finisce mai nel tuo binario.
  • Un test è una funzione chiamata TestXxx (la parte dopo Test non 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

MetodoSegna come fallitoFerma il test
t.Error, t.Errorfsìno, continua
t.Fatal, t.Fatalfsìsì, subito
t.Log, t.Logfnono, stampa solo con -v o in caso di fallimento
t.Skip, t.Skipfnosì, 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

ComandoCosa fa
go test -vstampa il nome, il risultato e l'output di t.Log di ogni test
go test -run TestSlugifyesegue i test il cui nome corrisponde all'espressione regolare
go test -run 'TestSlugifyTable/empty'esegue un singolo subtest
go test -count=1ignora i risultati in cache ed esegue di nuovo
go test -raceesegue con il rilevatore di data race
go test -shortchiede ai test lunghi di saltarsi da soli (if testing.Short() { t.Skip() })
go test -failfastsi ferma dopo il primo test fallito
go test -timeout 30sfallisce se l'esecuzione dura di più (default 10 minuti)
go test -coverstampa 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.go e stare nella cartella del pacchetto. slug_tests.go viene compilato nel pacchetto, non eseguito come test.
  • Minuscola dopo Test. Testslugify non è un test. TestSlugify e Test_slugify sì.
  • t.Fatal da un'altra goroutine. Lì segnala con t.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=1 quando 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.Setenv e t.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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA