Quando si scrivono unit test in Go, gestire dipendenze esterne come file system, orologi di sistema o comandi esterni può rivelarsi complesso. Questo articolo esplora tre approcci per affrontare il problema, mostrando infine perché il Command Pattern offre un’alternativa pulita e facilmente testabile ai mock complessi.
Vediamo un esempio: una funzione progettata per eseguire il benchmark di build incrementali.
func Benchmark(binary string, sourcePath string, resultPath string, numBuilds int) error {
if err := build(binary); err != nil {
return err
}
source, err := read(sourcePath)
if err != nil {
return err
}
deltas := []time.Duration{}
for i := 0; i < numBuilds; i++ {
if err := write(sourcePath, edit(source, i)); err != nil {
return err
}
start := now()
if err := build(binary); err != nil {
return err
}
delta := now().Sub(start)
deltas = append(deltas, delta)
if err := revert(sourcePath); err != nil {
return err
}
}
return write(resultPath, report(deltas))
}
Questa funzione Benchmark svolge diverse operazioni:
Testare direttamente questa funzione è difficile perché si appoggia a quattro dipendenze esterne:
build()read() e write()now()revert()Molti sviluppatori potrebbero limitarsi a testare le funzioni autonome edit e report, lasciando priva di test l’orchestrazione principale. Ecco come possiamo migliorare il design per rendere testabile l’intero processo.
Main
🔗A volte è possibile spingere le dipendenze verso l’alto, fino al chiamante (spesso main).
func Benchmark(binary string, sourcePath string, source string, numBuilds int) (string, error) {
// ... core loop logic remains ...
// returning the report string instead of writing it
return report(deltas), nil
}
func Main() error {
// setup and initial build moved here
source, _ := read(sourcePath)
result, _ := Benchmark(binary, sourcePath, source, 3)
return write(resultPath, result)
}
Verdetto: Sebbene questo aiuti, in questo caso non basta. Il ciclo di Benchmark ha comunque bisogno di interagire fondamentalmente con il sistema di build, l’orologio, il file system e il controllo di versione durante ogni iterazione.
L’approccio tradizionale orientato agli oggetti è la Dependency Injection. Si definisce un’interfaccia che rappresenta tutte le azioni esterne.
type Delegate interface {
Build(binary string) error
Read(path string) (string, error)
Write(path string, content string) error
Now() time.Time
Revert(path string) error
}
type BenchmarkRunner struct {
Delegate Delegate
}
func (b *BenchmarkRunner) Run(binary string, sourcePath string, resultPath string, numBuilds int) error {
// ... implementation using b.Delegate.Build(), b.Delegate.Now(), etc ...
}
Verdetto: Questo approccio funziona bene e consente di sostituire il delegato con un fake durante i test. Tuttavia, richiede di definire interfacce e implementazioni mock per ogni tipo di effetto collaterale (side effect), il che può portare a un’eccessiva proliferazione di interfacce.
Invece di definire interfacce specifiche per ogni azione, possiamo trattare ogni effetto collaterale come un comando generico che verrà eseguito da un “Runner” centrale.
Per prima cosa, definiamo cosa significa eseguire un comando:
type Runner interface {
Run(cmd Command) (Result, error)
}
Successivamente, definiamo i comandi specifici come semplici struct (o costanti):
type BuildCmd struct { Binary string }
type ReadCmd struct { Path string }
type WriteCmd struct { Path string; Content string }
type NowCmd struct {}
type RevertCmd struct { Path string }
Ora, ristrutturiamo la logica di benchmark affinché restituisca i comandi che vuole eseguire, o ceda il controllo a un runner:
type BenchmarkProcess struct {
runner Runner
}
func (p *BenchmarkProcess) Execute(binary, sourcePath, resultPath string, numBuilds int) error {
_, err := p.runner.Run(BuildCmd{Binary: binary})
if err != nil { return err }
readRes, err := p.runner.Run(ReadCmd{Path: sourcePath})
if err != nil { return err }
source := readRes.(string) // Type assertion needed here
// ... rest of the loop using p.runner.Run() ...
}
La vera forza di questo pattern emerge negli unit test. Basta scrivere un solo FakeRunner invece di implementare complesse interfacce mock per ogni singola dipendenza.
Il test diventa così una semplice lista di comandi attesi e risposte simulate:
func TestBenchmark(t *testing.T) {
// 1. Define the script: "When you see this command, return this result"
fakeRunner := NewFakeRunner([]Step{
{Cmd: BuildCmd{"binary"}, Err: nil},
{Cmd: ReadCmd{"source_path"}, Res: "source code", Err: nil},
{Cmd: WriteCmd{"source_path", "edited code"}, Err: nil},
{Cmd: NowCmd{}, Res: time.Unix(0, 0), Err: nil},
// ... and so on ...
})
p := BenchmarkProcess{runner: fakeRunner}
err := p.Execute("binary", "source_path", "result_path", 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// 2. Verify the runner consumed all expected steps
if !fakeRunner.IsDone() {
t.Fatalf("did not execute all expected commands")
}
}
Trasformando le chiamate di funzione in strutture dati (Commands), separi la logica di business dai suoi effetti collaterali.
Scarica l'edizione offline di Lo Stile di Vita di Kaue—una guida pratica per ottimizzare 🍏 Salute, 💰 Ricchezza, ❤️ Amore e 🧘 Significato.
Ricevi una notifica quando pubblico nuovi saggi, riassunti di libri e guide pratiche per vivere bene. Niente spam—annulla l'iscrizione in qualsiasi momento.
Hai una domanda, una prospettiva o un suggerimento? Condividi i tuoi pensieri qui sotto. I commenti utilizzano GitHub Discussions (senza pubblicità e senza spam)—clicca su Accedi con GitHub qui sotto per commentare o reagire (creare un account GitHub gratuito richiede meno di un minuto).
Aggiornato il 11 ott 2026.
AVVERTENZA LEGALE: Il contenuto di questo sito ha scopo esclusivamente informativo ed educativo e non costituisce consulenza medica, psicologica, finanziaria, di investimento, fiscale o legale. Le idee, la selezione e le opinioni qui presentate sono esclusivamente mie e non rappresentano quelle del mio datore di lavoro. I riassunti di libri, conferenze e articoli sono commenti educativi indipendenti e rimangono proprietà intellettuale dei rispettivi autori. Strumenti di IA possono essere utilizzati per assistere nella revisione, formattazione e sintesi; sebbene venga compiuto ogni sforzo per garantire l'accuratezza, possono verificarsi errori. PRIVACY: Questo sito utilizza servizi di terze parti per l'iscrizione facoltativa alla newsletter (follow.it), i commenti (giscus / GitHub Discussions) e i contenuti multimediali incorporati (YouTube), che possono trattare gli indirizzi IP e i cookie dei visitatori in conformità con le rispettive informative sulla privacy.