Testing
Il pacchetto testing
Go include un supporto nativo per i test attraverso il pacchetto testing e il comando go test. Non servono framework esterni: tutto cio che serve e gia integrato nel linguaggio. I file di test hanno il suffisso _test.go e vengono automaticamente esclusi dalla compilazione del programma finale.
Scrivere funzioni di test
Una funzione di test deve iniziare con Test seguito da una lettera maiuscola e accettare un parametro *testing.T:
// math.go
package math
func Somma(a, b int) int {
return a + b
}
func Moltiplica(a, b int) int {
return a * b
}
// math_test.go
package math
import "testing"
func TestSomma(t *testing.T) {
risultato := Somma(2, 3)
atteso := 5
if risultato != atteso {
t.Errorf("Somma(2, 3) = %d; atteso %d", risultato, atteso)
}
}
func TestMoltiplica(t *testing.T) {
risultato := Moltiplica(4, 5)
atteso := 20
if risultato != atteso {
t.Errorf("Moltiplica(4, 5) = %d; atteso %d", risultato, atteso)
}
}
I metodi principali di *testing.T sono:
t.Error()/t.Errorf(): segnala un errore ma continua l’esecuzione del test.t.Fatal()/t.Fatalf(): segnala un errore e interrompe immediatamente il test.t.Log()/t.Logf(): scrive un messaggio di log (visibile congo test -v).t.Skip()/t.Skipf(): salta il test.
Eseguire i test con go test
Per eseguire i test, si usa il comando go test:
# Esegue i test del pacchetto corrente
go test
# Output verboso
go test -v
# Esegue un test specifico
go test -run TestSomma
# Esegue i test di tutti i pacchetti
go test ./...
# Con il conteggio delle esecuzioni (senza cache)
go test -count=1 ./...
Table-Driven Tests
I table-driven tests sono un pattern idiomatico di Go che permette di testare piu casi con una singola funzione:
func TestSomma_TableDriven(t *testing.T) {
casi := []struct {
nome string
a, b int
atteso int
}{
{"positivi", 2, 3, 5},
{"zero", 0, 0, 0},
{"negativi", -1, -2, -3},
{"misto", -5, 10, 5},
{"grandi numeri", 1000000, 2000000, 3000000},
}
for _, tc := range casi {
t.Run(tc.nome, func(t *testing.T) {
risultato := Somma(tc.a, tc.b)
if risultato != tc.atteso {
t.Errorf("Somma(%d, %d) = %d; atteso %d",
tc.a, tc.b, risultato, tc.atteso)
}
})
}
}
Questo pattern offre molti vantaggi: e facile aggiungere nuovi casi, i nomi dei sottotesti rendono chiaro quale caso fallisce, e il codice rimane DRY (Don’t Repeat Yourself).
Subtests con t.Run
Il metodo t.Run permette di creare sottotests con nomi descrittivi:
func TestOperazioni(t *testing.T) {
t.Run("Somma", func(t *testing.T) {
if Somma(1, 1) != 2 {
t.Error("1 + 1 dovrebbe essere 2")
}
})
t.Run("Moltiplica", func(t *testing.T) {
if Moltiplica(2, 3) != 6 {
t.Error("2 * 3 dovrebbe essere 6")
}
})
}
Possiamo eseguire un singolo sottotesto dalla riga di comando:
go test -run TestOperazioni/Somma -v
Test Helpers
Le funzioni helper riducono la duplicazione nei test. In Go 1.9+ possiamo usare t.Helper() per migliorare i messaggi di errore:
func assertUguali(t *testing.T, ottenuto, atteso int) {
t.Helper() // Segnala che questa e una funzione helper
if ottenuto != atteso {
t.Errorf("ottenuto %d, atteso %d", ottenuto, atteso)
}
}
func TestConHelper(t *testing.T) {
assertUguali(t, Somma(2, 2), 4)
assertUguali(t, Somma(0, 0), 0)
assertUguali(t, Moltiplica(3, 3), 9)
}
Senza t.Helper(), il messaggio di errore indicherebbe la riga all’interno della funzione helper. Con t.Helper(), il messaggio punta alla riga nel test che ha chiamato l’helper.
Test Coverage
Go include strumenti integrati per misurare la copertura del codice:
# Mostra la percentuale di copertura
go test -cover
# Genera un file di profilo della copertura
go test -coverprofile=coverage.out
# Visualizza la copertura in formato HTML
go tool cover -html=coverage.out
# Mostra la copertura per funzione
go tool cover -func=coverage.out
Il report HTML colora il codice in verde (coperto) e rosso (non coperto), rendendo facile identificare le parti non testate.
Testable Examples
Go supporta gli esempi testabili, che servono sia come documentazione che come test:
func ExampleSomma() {
risultato := Somma(2, 3)
fmt.Println(risultato)
// Output: 5
}
func ExampleMoltiplica() {
risultato := Moltiplica(4, 5)
fmt.Println(risultato)
// Output: 20
}
Gli esempi vengono eseguiti da go test e verificano che l’output corrisponda al commento // Output:. Inoltre, appaiono automaticamente nella documentazione generata da go doc.
Setup e Teardown
Per eseguire codice prima e dopo i test, si usa TestMain:
func TestMain(m *testing.M) {
// Setup: eseguito prima di tutti i test
fmt.Println("Inizializzazione...")
// Esegui i test
codice := m.Run()
// Teardown: eseguito dopo tutti i test
fmt.Println("Pulizia...")
os.Exit(codice)
}
Per setup e teardown a livello di singolo test, si usa t.Cleanup:
func TestConCleanup(t *testing.T) {
// Setup
file, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal(err)
}
// Registra la funzione di cleanup
t.Cleanup(func() {
os.Remove(file.Name())
})
// Il test usa il file...
}
Conclusione
Il sistema di testing di Go e semplice ma potente. Con il pacchetto testing e il comando go test, abbiamo tutto il necessario per scrivere test efficaci senza dipendenze esterne. I table-driven tests, i sottotests e gli esempi testabili sono pattern idiomatici che ogni sviluppatore Go dovrebbe conoscere. La copertura del codice integrata aiuta a garantire che il codice sia ben testato prima del rilascio.