Meteora Web

Testing in Go — Test Unitari, Benchmark e Table-Driven Tests che Proteggono il Fatturato

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 cod...

Ing. Calogero Bono 7 मिनट में पढ़ें
Testing in Go — Test Unitari, Benchmark e Table-Driven Tests che Proteggono il Fatturato

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.

Zenithby Meteora Web आपके व्यवसाय का ऑपरेटिंग सिस्टम। सोशल मीडिया, क्लाइंट, बुकिंग और इनवॉइस एक ही प्लेटफ़ॉर्म पर। जिम, बार्बर और पेशेवरों के लिए। Zenith देखें → मुफ़्त डेमो · कार्ड की ज़रूरत नहीं

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ù.

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.

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.

BenchmarkCalcolaCommissione-8   100000000   12.5 ns/op   0 B/op   0 allocs/op

Se 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.

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.

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 -cover sul tuo progetto e identifica le funzioni con copertura zero. Scegline una e scrivi i test che mancano.
  • Configura la CI per eseguire go test ./... e go 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.

Zenithby Meteora Web आपके व्यवसाय का ऑपरेटिंग सिस्टम। सोशल मीडिया, क्लाइंट, बुकिंग और इनवॉइस एक ही प्लेटफ़ॉर्म पर। जिम, बार्बर और पेशेवरों के लिए। Zenith देखें → मुफ़्त डेमो · कार्ड की ज़रूरत नहीं

यह भी पढ़ें

ब्लॉग से

नवीनतम लेख