Go Testing 2026: Unit Test, Mock e Domande per Colloqui Tecnici

Una guida completa alle pratiche di testing in Go nel 2026, inclusi unit test con il pacchetto testing, mocking con gomock e domande frequenti nei colloqui tecnici.

Go Testing 2026: Unit Test, Mock e Domande per Colloqui

Il testing in Go si basa sul pacchetto standard testing, distribuito con ogni installazione di Go e che non richiede dipendenze esterne. A differenza dei framework in altri linguaggi, l'approccio di Go privilegia la semplicità: i file di test si trovano accanto al codice di produzione, le funzioni di test seguono una convenzione di denominazione, e il comando go test gestisce la scoperta e l'esecuzione.

Cosa cercano gli intervistatori

Candidati che scrivono test table-driven, utilizzano le interfacce per la dependency injection e comprendono quando il mocking aggiunge valore rispetto a quando aggiunge solo rumore.

Scrivere Unit Test con il pacchetto testing

Ogni file di test termina con _test.go e risiede nello stesso package del codice da testare. Le funzioni di test iniziano con Test seguito da un nome con iniziale maiuscola. Il parametro *testing.T fornisce metodi per segnalare i fallimenti.

calculator_test.gogo
package calculator

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

L'esecuzione di go test ./... esegue tutti i test nel modulo corrente. Il flag -v mostra i nomi dei singoli test e il loro stato di superamento/fallimento.

Test Table-Driven: Lo Standard di Go

I test table-driven riducono la duplicazione e rendono banale l'aggiunta di nuovi casi. Ogni riga nella tabella rappresenta uno scenario con input e output attesi. Questo pattern appare nella libreria standard di Go e nelle codebase di produzione in tutto il settore.

validator_test.gogo
package validator

import "testing"

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        {"valid email", "user@example.com", false},
        {"missing at sign", "userexample.com", true},
        {"empty string", "", true},
        {"unicode local part", "user@例え.jp", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.email)
            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
                    tt.email, err, tt.wantErr)
            }
        })
    }
}

Il metodo t.Run crea subtest che vengono eseguiti in modo indipendente. I subtest falliti riportano quale caso specifico è fallito senza interrompere gli altri casi.

Dependency Injection tramite Interfacce

Le interfacce Go permettono di testare codice che dipende da sistemi esterni: database, client HTTP, file system. Definire un'interfaccia minimale nel punto di utilizzo consente di sostituire le implementazioni reali con test double.

service.gogo
package order

// Repository definisce i metodi di accesso ai dati necessari a questo servizio
type Repository interface {
    FindByID(id string) (*Order, error)
    Save(order *Order) error
}

// Service gestisce la logica di business degli ordini
type Service struct {
    repo Repository
}

// NewService crea un servizio con il repository fornito
func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

// Process valida e salva un ordine
func (s *Service) Process(order *Order) error {
    if order.Total <= 0 {
        return ErrInvalidTotal
    }
    return s.repo.Save(order)
}

Il Service accetta qualsiasi tipo che soddisfi Repository. Il codice di produzione passa un'implementazione supportata da database. I test passano un mock o uno stub.

Mocking con gomock e mockgen

Il pacchetto gomock genera implementazioni mock dalle definizioni delle interfacce. Il comando mockgen legge i file sorgente e produce struct mock che registrano le chiamate e restituiscono valori configurati.

bash
# Genera mock per l'interfaccia Repository
mockgen -source=service.go -destination=mocks/repository_mock.go -package=mocks

Utilizzo del mock generato nei test:

service_test.gogo
package order

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "go.uber.org/mock/gomock"

    "yourmodule/order/mocks"
)

func TestService_Process_SavesValidOrder(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    order := &Order{ID: "123", Total: 99.99}

    // Aspettarsi che Save venga chiamato una volta con questo ordine, restituire nil
    mockRepo.EXPECT().
        Save(order).
        Return(nil).
        Times(1)

    svc := NewService(mockRepo)
    err := svc.Process(order)

    assert.NoError(t, err)
}

func TestService_Process_RejectsZeroTotal(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    // Nessuna chiamata attesa al repo poiché la validazione fallisce prima

    svc := NewService(mockRepo)
    err := svc.Process(&Order{ID: "456", Total: 0})

    assert.ErrorIs(t, err, ErrInvalidTotal)
}

Il mock verifica che Save sia stato chiamato esattamente una volta con l'argomento atteso. Se il codice sotto test non chiama mai Save, o lo chiama con argomenti diversi, il test fallisce.

Utilizzo di testify per Assertion e Suite

testify fornisce funzioni di assertion che producono messaggi di errore più chiari rispetto ai controlli if manuali. Il pacchetto assert continua l'esecuzione dopo i fallimenti. Il pacchetto require interrompe il test immediatamente.

user_test.gogo
package user

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestParseUser(t *testing.T) {
    input := `{"id": "u1", "name": "Alice"}`

    user, err := ParseUser(input)

    require.NoError(t, err, "parsing should not fail")
    assert.Equal(t, "u1", user.ID)
    assert.Equal(t, "Alice", user.Name)
    assert.Empty(t, user.Email, "email should default to empty")
}

La chiamata require.NoError interrompe il test se il parsing fallisce, prevenendo panic da puntatori nil nelle assertion successive.

Pronto a superare i tuoi colloqui su Go?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Testare Handler HTTP con httptest

Il pacchetto net/http/httptest fornisce ResponseRecorder per catturare l'output dell'handler e Server per eseguire un server di test locale. La maggior parte degli unit test utilizza ResponseRecorder per evitare l'overhead di rete.

handler_test.gogo
package api

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestHealthHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    rec := httptest.NewRecorder()

    HealthHandler(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.JSONEq(t, `{"status": "ok"}`, rec.Body.String())
}

Per handler che chiamano servizi esterni, si inietta un client mock o si utilizza httptest.NewServer per creare un backend fittizio.

Domande Frequenti nei Colloqui sul Testing in Go

Gli intervistatori valutano la conoscenza del testing attraverso domande che rivelano l'esperienza con codebase reali. Ecco i pattern che distinguono i candidati preparati:

"Come testeresti una funzione che chiama un'API esterna?"

Definire un'interfaccia per il client HTTP, iniettarla nella funzione o struct, e fornire un mock che restituisce risposte predeterminate. Questo evita le chiamate di rete e rende i test deterministici.

"Quando useresti uno stub rispetto a un mock?"

Gli stub restituiscono risposte predefinite senza verificare come sono stati chiamati. I mock verificano che metodi specifici siano stati chiamati con argomenti specifici. Si usano gli stub quando il valore di ritorno conta più dell'interazione. Si usano i mock quando verificare l'interazione è lo scopo del test.

"Cosa sono i test table-driven e perché Go li preferisce?"

I test table-driven definiscono i casi di test come dati in uno slice di struct. Riducono il boilerplate, rendono banale l'aggiunta di casi e producono output di test leggibili con t.Run. Il pattern si allinea con la preferenza di Go per codice esplicito e ripetitivo rispetto ad astrazioni elaborate.

"Come gestisci le fixture di test o il setup condiviso tra più test?"

Utilizzare TestMain per setup e teardown a livello di package. Utilizzare funzioni helper o test suite (da testify) per setup condiviso tra test correlati. Evitare lo stato globale che crea accoppiamento tra i test.

Preparazione per un colloquio Go? Il modulo Go Testing copre questi pattern in profondità con domande di pratica.

Eseguire Test con Coverage e Race Detection

Il comando go test accetta flag che rivelano percorsi di codice non testati e bug di concorrenza.

bash
# Eseguire test con report di coverage
go test -cover ./...

# Generare visualizzazione HTML della coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

# Rilevare race condition (più lento, ma trova bug reali)
go test -race ./...

Il race detector instrumenta gli accessi alla memoria e segnala letture e scritture concorrenti sulla stessa variabile. Eseguire i test con -race nella CI cattura bug che si manifestano solo con timing specifici.

Organizzare i Test in Codebase di Grandi Dimensioni

Man mano che i progetti crescono, l'organizzazione dei test influisce sulla manutenibilità:

  • Test same-package (package foo) hanno accesso a funzioni e campi non esportati. Da usare per unit test che verificano il comportamento interno.
  • Test external-package (package foo_test) importano il package come codice esterno. Da usare per test di integrazione e per verificare l'API pubblica.
  • Directory testdata memorizzano fixture come file JSON, output golden o database seed. Go le ignora durante il build.
  • Build tag separano gli unit test dai test di integrazione che richiedono sistemi esterni. Eseguire go test -tags=integration ./... per includerli.

Per approfondire le best practice di Go oltre il testing, l'articolo sui pattern di concorrenza tratta goroutine e channel.

Cosa Ricordare sul Testing in Go

  • Il pacchetto testing è distribuito con Go e non richiede dipendenze esterne
  • I test table-driven riducono la duplicazione e sono il pattern atteso nei colloqui
  • Le interfacce abilitano la dependency injection; definirle nel punto di utilizzo, non nell'implementazione
  • gomock genera mock dalle interfacce; testify fornisce assertion più pulite
  • httptest.ResponseRecorder cattura l'output degli handler senza overhead di rete
  • Il flag -race rileva bug di concorrenza che gli unit test altrimenti mancano
  • Le metriche di coverage guidano l'attenzione ma non garantiscono la correttezza

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Sfida del giorno

Sapresti trovare il bug in Go?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 24 agosto 2026

Tag

#go
#testing
#unit-tests
#mocking
#interview

Condividi

Articoli correlati