Testing in Go — Test Unitari, Benchmark e Table-Driven Tests che Proteggono il Fatturato
> cd .. / HUB_EDITORIALE > Visualizza in Inglese
Sviluppo di siti web

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

[2026-08-08] Author: Ing. Calogero Bono
> condividi
Zenithby Meteora Web Il sistema operativo della tua attività. Social, clienti, prenotazioni e fatture in un'unica piattaforma. Palestre, barber, professionisti. Scopri Zenith Demo gratis · senza carta

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

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

> condividi
Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // WEB AGENCY

Costruiamo la presenza digitale che la tua azienda merita.

Siti web, social, pubblicità online, e-commerce e hosting performante: ingegnerizzati con metodo da ingegneri informatici a Sciacca, per tutta Italia.

> MW_JOURNAL

> READ_ALL()