Unit Testing in Go: Struttura e Migliori Pratiche
Test con Go da basi a pattern avanzati
Il pacchetto di test integrato in Go fornisce un potente framework minimalista per scrivere test unitari senza dipendenze esterne. Ecco i fondamenti del testing, la struttura del progetto e i pattern avanzati per costruire applicazioni Go affidabili.

Perché il testing è importante in Go
La filosofia di Go enfatizza semplicità e affidabilità. La libreria standard include il pacchetto testing, rendendo il testing unitario un cittadino di prima classe nell’ecosistema Go. Il codice Go ben testato migliora la manutenibilità, cattura i bug precocemente e fornisce documentazione attraverso esempi. Se sei nuovo a Go, consulta il nostro Go Cheat Sheet per un riferimento rapido sui fondamenti del linguaggio.
Vantaggi principali del testing in Go:
- Supporto integrato: Non sono necessari framework esterni
- Esecuzione veloce: Esecuzione concorrente dei test per default
- Sintassi semplice: Minima quantità di codice boilerplate
- Strumenti ricchi: Report di copertura, benchmark e profilatura
- Amichevole per CI/CD: Integrazione facile con pipeline automatizzate
Struttura del progetto per i test in Go
I test in Go vivono accanto al codice produttivo con una chiara convenzione di nomi:
myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│ ├── helper.go
│ └── helper_test.go
└── models/
├── user.go
└── user_test.go
Convenzioni principali:
- I file di test terminano con
_test.go - I test sono nello stesso pacchetto del codice (o utilizzano il suffisso
_testper il testing nero) - Ogni file sorgente può avere un file di test corrispondente
Approcci al testing del pacchetto
Testing bianco (stesso pacchetto):
package calculator
import "testing"
// Può accedere alle funzioni e variabili non esportate
Testing nero (pacchetto esterno):
package calculator_test
import (
"testing"
"myproject/calculator"
)
// Può accedere solo alle funzioni esportate (consigliato per le API pubbliche)
Struttura di base dei test
Ogni funzione di test segue questo modello:
package calculator
import "testing"
// La funzione di test deve iniziare con "Test"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Metodi di testing.T:
t.Error()/t.Errorf(): Contrassegna il test come fallito ma continuat.Fatal()/t.Fatalf(): Contrassegna il test come fallito e ferma immediatamentet.Log()/t.Logf(): Output di log (visualizzato solo con il flag-v)t.Skip()/t.Skipf(): Salta il testt.Parallel(): Esegui il test in parallelo con altri test paralleli
Test a tabelle: Il modo Go
I test a tabelle sono l’approccio idiomatico in Go per testare diversi scenari. Con Go generics, puoi anche creare helper di test type-safe che funzionano su diversi tipi di dati:
func TestCalculate(t *testing.T) {
tests := []struct {
name string
a, b int
op string
expected int
wantErr bool
}{
{"addizione", 2, 3, "+", 5, false},
{"sottrazione", 5, 3, "-", 2, false},
{"moltiplicazione", 4, 3, "*", 12, false},
{"divisione", 10, 2, "/", 5, false},
{"divisione per zero", 10, 0, "/", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Calculate(tt.a, tt.b, tt.op)
if (err != nil) != tt.wantErr {
t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if result != tt.expected {
t.Errorf("Calculate(%d, %d, %q) = %d; want %d",
tt.a, tt.b, tt.op, result, tt.expected)
}
})
}
}
Vantaggi:
- Una singola funzione di test per diversi scenari
- Facile aggiunta di nuovi casi di test
- Documentazione chiara del comportamento previsto
- Organizzazione e manutenibilità dei test migliori
Esecuzione dei test
Comandi di base
# Esegui i test nella directory corrente
go test
# Esegui i test con output dettagliato
go test -v
# Esegui i test in tutte le sottodirectory
go test ./...
# Esegui un test specifico
go test -run TestAdd
# Esegui test che corrispondono a un pattern
go test -run TestCalculate/addizione
# Esegui i test in parallelo (predefinito è GOMAXPROCS)
go test -parallel 4
# Esegui i test con timeout
go test -timeout 30s
Copertura dei test
# Esegui i test con copertura
go test -cover
# Genera il profilo di copertura
go test -coverprofile=coverage.out
# Visualizza la copertura nel browser
go tool cover -html=coverage.out
# Mostra la copertura per funzione
go tool cover -func=coverage.out
# Imposta la modalità di copertura (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out
Flag utili
-short: Esegui i test contrassegnati conif testing.Short()-race: Abilita il rilevatore di corse (trova problemi di accesso concorrente)-cpu: Specifica i valori di GOMAXPROCS-count n: Esegui ciascun test n volte-failfast: Ferma al primo fallimento del test
Helper per i test e setup/teardown
Funzioni helper
Contrassegna le funzioni helper con t.Helper() per migliorare la segnalazione degli errori:
func assertEqual(t *testing.T, got, want int) {
t.Helper() // Questa riga è segnalata come chiamante
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestMath(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5) // La riga dell'errore punta qui
}
Setup e teardown
func TestMain(m *testing.M) {
// Codice di setup
setup()
// Esegui i test
code := m.Run()
// Codice di teardown
teardown()
os.Exit(code)
}
Fixtures per i test
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("setup test case")
return func(t *testing.T) {
t.Log("teardown test case")
}
}
func TestSomething(t *testing.T) {
teardown := setupTestCase(t)
defer teardown(t)
// Codice del test
}
Mocking e iniezione di dipendenze
Mocking basato su interfacce
Quando si testa il codice che interagisce con database, l’uso di interfacce rende facile la creazione di implementazioni mock. Se stai lavorando con PostgreSQL in Go, consulta il nostro confronto tra ORMs Go per scegliere la libreria database giusta con buona testabilità.
// Codice produttivo
type Database interface {
GetUser(id int) (*User, error)
}
type UserService struct {
db Database
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.db.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
// Codice di test
type MockDatabase struct {
users map[int]*User
}
func (m *MockDatabase) GetUser(id int) (*User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, errors.New("utente non trovato")
}
func TestGetUserName(t *testing.T) {
mockDB := &MockDatabase{
users: map[int]*User{
1: {ID: 1, Name: "Alice"},
},
}
service := &UserService{db: mockDB}
name, err := service.GetUserName(1)
if err != nil {
t.Fatalf("errore inaspettato: %v", err)
}
if name != "Alice" {
t.Errorf("got %s, want Alice", name)
}
}
Librerie di testing popolari
Testify
La libreria di testing più popolare in Go per affermazioni e mock:
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestWithTestify(t *testing.T) {
result := Add(2, 3)
assert.Equal(t, 5, result, "dovrebbero essere uguali")
assert.NotNil(t, result)
}
// Esempio di mock
type MockDB struct {
mock.Mock
}
func (m *MockDB) GetUser(id int) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
Altri strumenti
- gomock: Framework di mocking di Google con generazione del codice
- httptest: Libreria standard per testare i gestori HTTP
- testcontainers-go: Testing di integrazione con container Docker
- ginkgo/gomega: Framework di testing BDD-style
Quando si testano integrazioni con servizi esterni come modelli AI, è necessario mockare o stubbare quelle dipendenze. Ad esempio, se stai utilizzando Ollama in Go, considera la creazione di wrapper di interfacce per rendere il tuo codice più testabile.
Test di benchmark
Go include un supporto integrato per i benchmark:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Esegui i benchmark
// go test -bench=. -benchmem
L’output mostra le iterazioni al secondo e le allocazioni di memoria.
Linee guida
- Scrivi test a tabelle: Utilizza il modello slice di struct per diversi casi di test
- Usa t.Run per i sottotest: Migliore organizzazione e possibilità di eseguire i sottotest in modo selettivo
- Testa prima le funzioni esportate: Concentrati sul comportamento dell’API pubblica
- Mantieni i test semplici: Ogni test dovrebbe verificare una sola cosa
- Usa nomi significativi per i test: Descrivi cosa viene testato e l’esito previsto
- Non testare i dettagli dell’implementazione: Testa il comportamento, non gli interni
- Usa interfacce per le dipendenze: Rende più facile il mocking
- Mirare a una buona copertura, ma qualità prima della quantità: La copertura al 100% non significa assenza di bug
- Esegui i test con il flag -race: Cattura i problemi di concorrenza precocemente
- Usa TestMain per il setup costoso: Evita di ripetere il setup in ogni test
Esempio: Suite di test completa
package user
import (
"errors"
"testing"
)
type User struct {
ID int
Name string
Email string
}
func ValidateUser(u *User) error {
if u.Name == "" {
return errors.New("il nome non può essere vuoto")
}
if u.Email == "" {
return errors.New("l'email non può essere vuota")
}
return nil
}
// File di test: user_test.go
func TestValidateUser(t *testing.T) {
tests := []struct {
name string
user *User
wantErr bool
errMsg string
}{
{
name: "utente valido",
user: &User{ID: 1, Name: "Alice", Email: "alice@example.com"},
wantErr: false,
},
{
name: "nome vuoto",
user: &User{ID: 1, Name: "", Email: "alice@example.com"},
wantErr: true,
errMsg: "il nome non può essere vuoto",
},
{
name: "email vuota",
user: &User{ID: 1, Name: "Alice", Email: ""},
wantErr: true,
errMsg: "l'email non può essere vuota",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.user)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && err.Error() != tt.errMsg {
t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
}
})
}
}
Link utili
- Documentazione ufficiale del pacchetto di testing in Go
- Blog Go: Test a tabelle
- Repository GitHub di Testify
- Documentazione di GoMock
- Impara Go con i test
- Strumento di copertura di Go
- Go Cheat Sheet
- Confronto tra ORMs Go per PostgreSQL: GORM vs Ent vs Bun vs sqlc
- SDK Go per Ollama - confronto con esempi
- Applicazioni CLI in Go con Cobra & Viper
- Generics in Go: casi d’uso e pattern
Conclusione
Il framework di testing in Go fornisce tutto ciò di cui hai bisogno per un testing unitario completo con un minimo di configurazione. Seguendo gli idiomi Go come i test a tabelle, utilizzando interfacce per il mocking e sfruttando gli strumenti integrati, puoi creare suite di test mantenibili e affidabili che crescono insieme al tuo codicebase.
Queste pratiche di testing si applicano a tutti i tipi di applicazioni Go, dalle web service alle applicazioni CLI costruite con Cobra & Viper. Il testing degli strumenti a riga di comando richiede pattern simili con un ulteriore focus sul testing di input/output e parsing delle opzioni.
Inizia con test semplici, aggiungi gradualmente la copertura e ricorda che il testing è un investimento nella qualità del codice e nella fiducia del team di sviluppo. L’accento della comunità Go sul testing rende più facile mantenere i progetti a lungo termine e collaborare efficacemente con i membri del team.