Il tuo backend Go funziona in locale. Poi lo deployi in produzione e qualcosa si rompe — magari un endpoint che gestisce i pagamenti o una funzione che calcola le commissioni. Il problema non è il codice che hai scritto, ma quello che non hai verificato prima. Noi, di Meteora Web, lo vediamo ogni giorno: le PMI italiane trattano i test come un costo, non come un investimento. Ma un bug in produzione costa molto più di un'ora di test. In questa guida vediamo come usare il testing in Go — test unitari, benchmark e table-driven tests — per scrivere codice che regge al traffico e ai margini stretti.
Perché i test unitari in Go sono diversi da quelli di altri linguaggi?
Go è nato per la produzione. Il toolchain di testing è integrato nel linguaggio: niente framework esterni obbligatori, niente configurazioni infinite. Il comando go test fa tutto. Ma la differenza sostanziale è un'altra: i test in Go sono codice normale. Non c'è magia, non c'è reflection implicita. Scrivi una funzione, la chiami, verifichi il risultato. Questo significa che i test sono leggibili anche da chi non li ha scritti — e in un'azienda che produce, la leggibilità è una forma di manutenzione economica.
Il pattern più potente in Go è il table-driven test: definisci una tabella di casi di input e output attesi, poi un unico loop li esegue tutti. Sembra semplice, ma cambia tutto. Un test per ogni caso, un solo punto di manutenzione.
package main
import "testing"
func TestCalcolaCommissione(t *testing.T) {
tests := []struct {
name string
importo float64
atteso float64
}{
{"importo zero", 0, 0},
{"importo piccolo", 100, 2.5},
{"importo medio", 1000, 25},
{"importo grande", 10000, 250},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
risultato := CalcolaCommissione(tt.importo)
if risultato != tt.atteso {
t.Errorf("atteso %.2f, ottenuto %.2f", tt.atteso, risultato)
}
})
}
}Questo pattern ti permette di aggiungere un caso nuovo in una riga, senza duplicare codice. E quando un bug arriva dalla produzione, il primo passo è scrivere il test che lo riproduce — poi lo sistemi. Così il bug non torna più.
Sponsored Protocol
Come strutturare i test unitari in un progetto Go reale
La convenzione è semplice: un file main_test.go accanto al file che testa. Ma nei progetti veri, con più package, la struttura segue la logica del dominio. Noi usiamo una regola pratica: un test per ogni funzione pubblica, e test mirati per le funzioni private che contengono logica di business. Le funzioni private che chiamano solo altre funzioni non hanno bisogno di test propri — il test della funzione pubblica le copre.
Un errore comune che vediamo nei progetti che ci arrivano: test che dipendono dall'ambiente. Se il tuo test chiama un database o un'API esterna, non è un test unitario — è un test di integrazione. I test unitari devono essere veloci, deterministici e isolati. Se hai bisogno di un database, usa un'interfaccia e un mock.
type Store interface {
GetUser(id int) (User, error)
}
type MockStore struct {
user User
err error
}
func (m MockStore) GetUser(id int) (User, error) {
return m.user, m.err
}Con un'interfaccia e un mock, il tuo test verifica la logica del tuo codice, non il comportamento di un servizio esterno. E se il servizio esterno cambia, il tuo test non si rompe — il tuo codice continua a funzionare.
Sponsored Protocol
Come si scrivono benchmark che misurano davvero la performance?
Un benchmark in Go non è un'opinione — è una misura. Il framework di benchmarking è integrato e ti dà numeri precisi: tempo per operazione, allocazioni, byte allocati. Ma la domanda giusta non è "quanto è veloce?", è "quanto è veloce rispetto a cosa?" e "quanto costa in termini di risorse?".
Nel nostro lavoro, i benchmark servono per decisioni concrete: un endpoint che risponde in 200ms va bene, ma se il traffico raddoppia? E se il tuo servizio gestisce commissioni per un e-commerce, un algoritmo lento significa utenti che aspettano — e utenti che aspettano sono utenti che abbandonano il carrello.
func BenchmarkCalcolaCommissione(b *testing.B) {
for i := 0; i < b.N; i++ {
CalcolaCommissione(1000)
}
}Il loop b.N è gestito automaticamente: il framework esegue la funzione abbastanza volte da ottenere una misura affidabile. Poi lanci go test -bench=. e leggi i risultati. Ma attenzione: i benchmark vanno eseguiti in un ambiente stabile. Se il tuo laptop è sotto carico, i numeri non sono affidabili. Noi li eseguiamo sempre in CI, su una macchina dedicata, con lo stesso carico di produzione.
Come leggere i risultati di un benchmark e prendere decisioni
Il output di go test -bench=. mostra il tempo per operazione e le allocazioni. Le allocazioni sono spesso il vero collo di bottiglia: ogni allocazione è una pausa per il garbage collector. Se una funzione chiamata milioni di volte alloca memoria inutilmente, il costo è enorme. Il benchmark ti dice esattamente dove intervenire.
Sponsored Protocol
BenchmarkCalcolaCommissione-8 100000000 12.5 ns/op 0 B/op 0 allocs/opSe vedi 0 allocs/op, sei a posto. Se vedi allocazioni, usa go test -bench=. con -benchmem per vedere i dettagli, poi ottimizza la funzione. Ma ricorda: ottimizzare prima di misurare è una scommessa persa. Prima misuri, poi decidi se ne vale la pena.
Quali errori comuni evitare nei test Go?
Il primo errore è non scrivere test perché "il codice funziona". Il secondo è scrivere test che non testano nulla — test che passano sempre, indipendentemente dal codice. Questo succede quando il test verifica l'implementazione, non il comportamento. Un test che controlla i dettagli interni di una funzione si rompe a ogni refactoring, anche quando il comportamento è corretto.
Il terzo errore è testare troppo: test che coprono ogni riga di codice, inclusi i casi impossibili. La copertura al 100% è un mito costoso. Noi puntiamo a coprire la logica di business critica — calcoli, decisioni, flussi — non le funzioni banali. Un test per una funzione che ritorna una costante è tempo perso.
Il quarto errore è ignorare i test in CI. Un test che non viene eseguito a ogni push è un test che non esiste. Configura la pipeline per eseguire go test ./... e go vet ./... a ogni commit. Se un test fallisce, la build si blocca. Sembra rigido, ma è l'unico modo per garantire che il codice in produzione sia quello testato.
Come integrare i test Go nel flusso di lavoro del tuo team?
I test non sono un compito separato: sono parte del lavoro di sviluppo. Nel nostro flusso, ogni feature parte con la definizione dei casi di test — prima di scrivere il codice. Questo è il test-driven development applicato con pragmatismo: scrivi il test che descrive il comportamento atteso, poi scrivi il codice che lo fa passare. Il risultato è codice più semplice, perché scrivi solo quello che serve a passare il test.
Sponsored Protocol
Per i team che lavorano su branch, la regola è: nessun merge senza test verdi. Se usi Gitflow o trunk-based development, come abbiamo spiegato nella nostra guida sulle strategie di branching, i test sono il gate di qualità. Un branch con test falliti non entra in main. Questo protegge la produzione e riduce i conflitti — perché il codice rotto non arriva mai al merge.
E quando un test fallisce in CI, non è un problema del tester: è un problema del team. Lo si risolve insieme, subito, non si rimanda. Nel nostro lavoro con le PMI, vediamo che questa disciplina paga: meno bug in produzione, meno tempo speso a spegnere incendi, più tempo per costruire.
Come usare la copertura del codice senza diventarne schiavo?
La copertura è una metrica utile, ma non è l'obiettivo. go test -cover ti dice quale percentuale di righe è stata eseguita dai test. Ma una copertura alta non significa test buoni — significa solo che hai eseguito molto codice. Noi usiamo la copertura per trovare le zone morte: funzioni mai testate, branch mai percorsi. Poi decidiamo se vale la pena testarle.
La regola pratica: la logica di business deve avere copertura alta, il resto può essere più basso. Un calcolo di commissioni, una validazione di input, una decisione di routing — questi devono essere coperti al 100%. Una funzione che logga un messaggio? Non serve testarla. Il tempo che spendi a testare codice banale è tempo che non spendi a testare ciò che genera fatturato.
Sponsored Protocol
Per misurare la copertura in CI, usa go test -coverprofile=coverage.out e poi go tool cover -func=coverage.out per vedere il dettaglio per funzione. Se una funzione critica ha copertura bassa, è un campanello d'allarme — non un numero da ignorare.
Cosa fare adesso
I test non sono un costo: sono un investimento che paga ogni volta che eviti un bug in produzione. Ecco le azioni immediate:
- Scrivi un table-driven test per la funzione più critica del tuo backend — quella che gestisce pagamenti o calcoli. Aggiungi almeno 5 casi, inclusi i bordi.
- Esegui
go test -coversul tuo progetto e identifica le funzioni con copertura zero. Scegline una e scrivi i test che mancano. - Configura la CI per eseguire
go test ./...ego vet ./...a ogni push. Se non hai una CI, inizia con un semplice script locale. - Misura un benchmark sulla funzione che chiami più spesso. Se ha allocazioni, ottimizzala — e verifica con il benchmark che la modifica ha funzionato.
- Leggi la documentazione ufficiale su testing in Go e su il package testing per approfondire i dettagli.
E se vuoi vedere come tutto questo si inserisce in un progetto backend completo, dai un'occhiata alla nostra guida su Go per backend — lì trovi il quadro completo, dalla concorrenza alle API REST. Noi, di Meteora Web, costruiamo backend che reggono al traffico e ai margini — e i test sono il primo passo.