Beim Schreiben von Unit-Tests in Go kann der Umgang mit externen Abhängigkeiten wie Dateisystemen, Systemuhren oder externen Befehlen knifflig sein. Dieser Beitrag beleuchtet drei Ansätze zur Lösung dieses Problems und zeigt schließlich, warum das Command-Pattern (Befehlsmuster) eine saubere, gut testbare Alternative zu komplexen Mocks bietet.
Schauen wir uns ein Beispiel an: eine Funktion, die inkrementelle Builds benchmarkt.
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))
}
Diese Funktion Benchmark erledigt mehrere Dinge:
Diese Funktion direkt zu testen ist schwierig, weil sie von vier externen Systemen abhängt:
build()read() und write()now()revert()Viele Entwickler würden hier vielleicht nur die in sich geschlossenen Hilfsfunktionen edit und report testen und die eigentliche Orchestrierungslogik ungetestet lassen. Schauen wir uns an, wie wir das Design verbessern können, um den gesamten Ablauf testbar zu machen.
Main verschieben
🔗Manchmal lassen sich Abhängigkeiten einfach nach oben an den Aufrufer (häufig main) delegieren.
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)
}
Fazit: Das hilft zwar ein wenig, reicht hier aber nicht aus. Die Benchmark-Schleife muss in jedem Durchlauf weiterhin zwingend mit dem Build-System, der Systemuhr, dem Dateisystem und der Versionsverwaltung interagieren.
Der klassische objektorientierte Ansatz ist Dependency Injection. Man definiert ein Interface, das sämtliche externen Aktionen abbildet.
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 ...
}
Fazit: Das funktioniert gut und ermöglicht es, für Tests einen Fake-Delegate einzusetzen. Allerdings müssen dafür Interfaces und Mock-Implementierungen für jede einzelne Art von Seiteneffekt definiert werden, was schnell zu aufgeblähten Interfaces führt.
Anstatt für jede Aktion eigene Methoden in Interfaces zu definieren, können wir jeden Seiteneffekt als generisches Kommando (Command) modellieren, das von einem zentralen „Runner" ausgeführt wird.
Zuerst definieren wir, was es bedeutet, ein Kommando auszuführen:
type Runner interface {
Run(cmd Command) (Result, error)
}
Als Nächstes definieren wir unsere konkreten Kommandos als einfache Structs (oder Konstanten):
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 }
Nun bauen wir die Benchmark-Logik so um, dass sie die gewünschten Kommandos an den Runner übergibt:
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() ...
}
Die wahre Stärke dieses Musters zeigt sich in den Unit-Tests. Man muss nur einen einzigen FakeRunner schreiben, anstatt komplexe Mock-Interfaces für jede einzelne Abhängigkeit zu implementieren.
Der Test wird so zu einer einfachen Liste erwarteter Kommandos und vorbereiteter Antworten:
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")
}
}
Indem man Funktionsaufrufe in Datenstrukturen (Commands) verwandelt, entkoppelt man die Geschäftslogik sauber von ihren Seiteneffekten.
Lade die kuratierte Offline-Ausgabe von Kaues Lebensweise herunter – ein praktischer Leitfaden zur Optimierung von 🍏 Gesundheit, 💰 Wohlstand, ❤️ Liebe und 🧘 Sinn.
Erhalte eine Benachrichtigung, wenn ich neue Essays, Buchzusammenfassungen und praktische Leitfäden für ein gutes Leben veröffentliche. Kein Spam – jederzeit abbestellbar.
Hast du eine Frage, eine Perspektive oder einen Vorschlag? Teile deine Gedanken unten mit. Die Kommentare werden über GitHub Discussions bereitgestellt (werbefrei und spamfrei) – klicke unten auf Mit GitHub anmelden, um zu kommentieren oder zu reagieren (ein kostenloses GitHub-Konto ist in weniger als einer Minute erstellt).
Aktualisiert am 11.10.2026.
HAFTUNGSAUSSCHLUSS: Die Inhalte auf dieser Website dienen ausschließlich Informations- und Bildungszwecken und stellen keine medizinische, psychologische, finanzielle, steuerliche oder rechtliche Beratung sowie keine Anlageberatung dar. Die hier präsentierten Ideen, Zusammenstellungen und Meinungen sind meine eigenen und spiegeln nicht die meines Arbeitgebers wider. Zusammenfassungen von Büchern, Vorträgen und Artikeln sind unabhängige Bildungskommentare und bleiben geistiges Eigentum der jeweiligen Autoren. KI-Tools können zur Unterstützung bei der Bearbeitung, Formatierung und Synthese eingesetzt werden; obwohl größte Sorgfalt auf Richtigkeit gelegt wird, können Fehler auftreten. DATENSCHUTZ: Diese Website nutzt Dienste von Drittanbietern für optionale Newsletter-Abonnements (follow.it), Kommentare (giscus / GitHub Discussions) und eingebettete Medien (YouTube), die IP-Adressen und Cookies der Besucher gemäß ihren jeweiligen Datenschutzrichtlinien verarbeiten können.