Der Code, der getestet wird
Die Testwerkzeuge von Go gehören zur Standard-Toolchain: das Paket testing und der Befehl go test. Kein Framework zu installieren, keine Assertion-Bibliothek nötig.
Die Beispiele auf dieser Seite testen eine kleine Funktion, Slugify, die einen Titel in einen URL-Slug umwandelt. Hier ist sie, ausführbar, mit einer selbst gebauten Prüfung in main:
Der Editor im Browser führt main aus; go test kann er nicht ausführen. Alles Weitere ist so geschrieben, wie es in einem echten Projekt steht, mit der Terminalausgabe nach jedem Befehl. Alles wurde mit Go 1.24 ausgeführt.
Der erste Test
In einem Projekt liegt die Funktion in einem Paket, und ihre Tests liegen daneben in einer Datei, deren Name auf _test.go endet:
textutil/
├── go.mod module example.com/textutil
├── slug.go package textutil, func Slugify
└── slug_test.go package textutil, the tests
// slug_test.go
package textutil
import "testing"
func TestSlugify(t *testing.T) {
got := Slugify("Hello, World!")
want := "hello-world"
if got != want {
t.Errorf("Slugify(%q) = %q, want %q", "Hello, World!", got, want)
}
}
Die Regeln, nach denen go test arbeitet:
- Nur Dateien, die auf
_test.goenden, sind Testdateien.go buildignoriert sie, also landet Testcode nie in deinem Binary. - Ein Test ist eine Funktion namens
TestXxx(der Teil nachTestdarf nicht mit einem Kleinbuchstaben beginnen), die ein einziges*testing.Tnimmt. - Ein Test besteht, außer er ruft eine der Methoden für Fehlschläge auf oder löst eine Panic aus.
go test # the package in the current directory
go test ./... # every package in the module
$ go test
PASS
ok example.com/textutil 0.318s
Das Format der Fehlermeldung Func(input) = got, want expected ist die Go-Konvention. Nimm die Eingabe in die Meldung auf: Ein Fehlschlag, der nur got "a", want "b" sagt, schickt dich zurück in den Code, um herauszufinden, welche Eingabe es war.
t.Errorf gegenüber t.Fatalf
| Methode | Markiert als fehlgeschlagen | Stoppt den Test |
|---|---|---|
t.Error, t.Errorf | ja | nein, läuft weiter |
t.Fatal, t.Fatalf | ja | ja, sofort |
t.Log, t.Logf | nein | nein, Ausgabe nur mit -v oder bei einem Fehlschlag |
t.Skip, t.Skipf | nein | ja, als übersprungen gemeldet |
Nimm standardmäßig Errorf, damit ein Lauf jedes falsche Feld meldet. Nimm Fatalf, wenn der Rest des Tests nicht funktionieren kann, typischerweise nach einem unerwarteten Fehler:
func TestCountWords(t *testing.T) {
path := writeFile(t, "doc.txt", "the quick brown\nfox jumps\n")
got, err := CountWords(path)
if err != nil {
t.Fatalf("CountWords: unexpected error: %v", err) // stop: got is meaningless
}
if got != 5 {
t.Errorf("CountWords = %d, want 5", got)
}
}
Fatal stoppt den Test über runtime.Goexit, muss also aus der eigenen Goroutine des Tests aufgerufen werden. Aus einer Goroutine, die du gestartet hast, meldest du mit t.Error und kehrst zurück.
Tabellengesteuerte Tests mit t.Run
Die meisten Go-Tests sind Tabellen: ein Slice von Fällen, eine Schleife und t.Run, das jedem Fall einen eigenen Namen gibt.
func TestSlugifyTable(t *testing.T) {
tests := []struct {
name, in, want string
}{
{"simple", "Go Testing", "go-testing"},
{"punctuation", "What's new in Go 1.24?", "what-s-new-in-go-1-24"},
{"extra spaces", " lots of space ", "lots-of-space"},
{"non-ascii dropped", "Café au lait", "caf-au-lait"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Slugify(tt.in); got != tt.want {
t.Errorf("Slugify(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
Einen Fall hinzuzufügen ist eine Zeile. Jeder Subtest wird einzeln gemeldet, ein Fatal darin beendet nur diesen Subtest, und du kannst einen einzelnen per Namen ausführen. Führ alles mit -v aus:
$ go test -v
=== RUN TestSlugify
--- PASS: TestSlugify (0.00s)
=== RUN TestSlugifyTable
=== RUN TestSlugifyTable/simple
=== RUN TestSlugifyTable/punctuation
=== RUN TestSlugifyTable/extra_spaces
=== RUN TestSlugifyTable/non-ascii_dropped
=== RUN TestSlugifyTable/empty
--- PASS: TestSlugifyTable (0.00s)
--- PASS: TestSlugifyTable/simple (0.00s)
--- PASS: TestSlugifyTable/punctuation (0.00s)
--- PASS: TestSlugifyTable/extra_spaces (0.00s)
--- PASS: TestSlugifyTable/non-ascii_dropped (0.00s)
--- PASS: TestSlugifyTable/empty (0.00s)
=== RUN TestCountWords
--- PASS: TestCountWords (0.00s)
=== RUN TestCountWordsMissingFile
--- PASS: TestCountWordsMissingFile (0.00s)
=== RUN ExampleSlugify
--- PASS: ExampleSlugify (0.00s)
PASS
ok example.com/textutil 0.182s
Leerzeichen in Subtest-Namen werden zu Unterstrichen. Schlägt ein Fall fehl, nennt die Ausgabe den Subtest und die Zeile:
--- FAIL: TestSlugifyTable (0.00s)
--- FAIL: TestSlugifyTable/underscore (0.00s)
slug_test.go:27: Slugify("snake_case_name") = "snake-case-name", want "snake_case_name"
FAIL
FAIL example.com/textutil 0.289s
Seit Go 1.22 hat jeder Schleifendurchlauf sein eigenes tt, also sehen Closures in t.Run den richtigen Fall, auch wenn Subtests parallel laufen. Älterer Code hat deshalb oft tt := tt am Anfang des Schleifenrumpfs; das ist nicht mehr nötig.
Um Subtests parallel auszuführen, ruf t.Parallel() am Anfang der Subtest-Funktion auf. Mach das nur, wenn die Fälle unabhängig und langsam genug sind, dass es sich lohnt.
Flags von go test, die du nutzen wirst
| Befehl | Macht |
|---|---|
go test -v | gibt Name, Ergebnis und t.Log-Ausgabe jedes Tests aus |
go test -run TestSlugify | führt Tests aus, deren Name zum regulären Ausdruck passt |
go test -run 'TestSlugifyTable/empty' | führt einen Subtest aus |
go test -count=1 | ignoriert zwischengespeicherte Ergebnisse und führt erneut aus |
go test -race | läuft mit dem Data Race Detector |
go test -short | sagt langen Tests, dass sie sich selbst überspringen sollen (if testing.Short() { t.Skip() }) |
go test -failfast | stoppt nach dem ersten fehlgeschlagenen Test |
go test -timeout 30s | schlägt fehl, wenn der Lauf länger dauert (Standard 10 Minuten) |
go test -cover | gibt die Anweisungsabdeckung aus |
go test -bench=. | führt zusätzlich Benchmarks aus |
Nennst du Pakete (go test ., go test ./...), speichert go test Ergebnisse für Pakete zwischen, deren Code und Eingaben sich nicht geändert haben, und gibt dahinter (cached) aus. Ein schlichtes go test ohne Argumente cacht nie. Der Cache verfolgt die Dateien und Umgebungsvariablen, die ein Test über das Paket os liest, aber keinen externen Zustand wie eine Datenbank oder einen Netzwerkdienst. Ein Test, der von so etwas abhängt, kann also aus dem Cache bestehen, obwohl er jetzt fehlschlagen würde; -count=1 erzwingt einen echten Lauf.
Helfer, temporäre Verzeichnisse und Aufräumen
Der Aufruf writeFile in TestCountWords oben ist ein Test-Helfer:
// writeFile is a test helper: t.Helper makes failures point at the caller.
func writeFile(t *testing.T, name, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name) // removed automatically after the test
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("writing %s: %v", name, err)
}
return path
}
t.Helper()markiert die Funktion als Helfer, damit ein Fehlschlag an der Zeile im Test gemeldet wird, die ihn aufgerufen hat, nicht im Helfer.t.TempDir()legt ein frisches Verzeichnis an und löscht es, wenn der Test endet. Tests müssen nie selbst Dateien aufräumen.t.Cleanup(func() { ... })registriert jedes andere Aufräumen (einen Server schließen, eine Tabelle löschen). Cleanups laufen nach dem Test und seinen Subtests, das zuletzt registrierte zuerst.t.Setenv("KEY", "value")setzt eine Umgebungsvariable nur für diesen Test und stellt sie danach wieder her.t.Context()(Go 1.24) gibt einen Context zurück, der kurz vor den Cleanups abgebrochen wird.
Fixture-Dateien für Tests gehören in ein Verzeichnis namens testdata neben den Tests. Das Tool go ignoriert es als Paket, und Tests laufen mit dem Paketverzeichnis als Arbeitsverzeichnis, also funktioniert os.ReadFile("testdata/input.json").
Fehler und HTTP-Handler testen
Prüf Fehler mit errors.Is oder errors.As, genauso wie es der aufrufende Code tun würde:
func TestCountWordsMissingFile(t *testing.T) {
_, err := CountWords(filepath.Join(t.TempDir(), "nope.txt"))
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("err = %v, want fs.ErrNotExist", err)
}
}
Fehler-Strings zu vergleichen bricht, sobald jemand eine Meldung umformuliert.
Für HTTP-Handler liefert net/http/httptest einen gefälschten ResponseWriter, mit dem du den Handler direkt aufrufen kannst, ganz ohne Netzwerk:
func TestHealth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
if body := rec.Body.String(); body != "ok\n" {
t.Errorf("body = %q, want %q", body, "ok\n")
}
}
httptest.NewServer startet einen echten Server auf localhost, um Client-Code zu testen. Die Seite zum HTTP-Client nutzt ihn für jedes Beispiel.
Coverage
$ go test -cover
PASS
coverage: 100.0% of statements
ok example.com/textutil 0.578s
$ go test -coverprofile=cover.out
$ go tool cover -func=cover.out
example.com/textutil/slug.go:11: Slugify 100.0%
example.com/textutil/words.go:9: CountWords 100.0%
total: (statements) 100.0%
$ go tool cover -html=cover.out # opens a browser with covered lines in green
Coverage zeigt dir, welche Zeilen nie gelaufen sind, und das hilft, ungetestete Zweige zu finden. Eine hohe Zahl sagt dir nicht, dass die Prüfungen etwas taugen: Ein Test, der jede Funktion aufruft und nichts prüft, erreicht 100 %.
Benchmarks
Ein Benchmark ist func BenchmarkXxx(b *testing.B). Go 1.24 hat b.Loop eingeführt, das jetzt die empfohlene Form ist:
func BenchmarkSlugify(b *testing.B) {
for b.Loop() {
Slugify("The Go Programming Language, 2nd Edition")
}
}
b.Loop führt den Rumpf so oft aus, wie für eine stabile Messung nötig ist, nimmt Setup-Code vor der Schleife aus der Zeitmessung heraus und verhindert, dass der Compiler den Aufruf wegoptimiert. Code, der vor Go 1.24 geschrieben wurde, nutzt for i := 0; i < b.N; i++. Das funktioniert weiterhin, braucht aber nach teurem Setup ein b.ResetTimer() und kann durch die Eliminierung toten Codes getäuscht werden.
Mit einem schlichten go test laufen Benchmarks nicht. Fordere sie an und überspring die normalen Tests mit -run='^$':
$ go test -bench=. -benchmem -run='^$'
goos: darwin
goarch: arm64
pkg: example.com/textutil
cpu: Apple M4
BenchmarkSlugify-10 5061945 238.1 ns/op 168 B/op 5 allocs/op
PASS
ok example.com/textutil 1.410s
Die Spalten sind: Name mit angehängtem GOMAXPROCS, ausgeführte Iterationen, Zeit pro Aufruf, allokierte Bytes pro Aufruf, Allokationen pro Aufruf. Die Zahlen hängen vom Rechner ab; vergleiche Läufe auf demselben. Um vorher und nachher zuverlässig zu vergleichen, führ jede Seite mehrmals mit -count=10 aus und gib beide Ausgaben an benchstat (golang.org/x/perf/cmd/benchstat).
Examples sind auch Tests
Eine Example-Funktion gibt etwas aus und deklariert die erwartete Ausgabe in einem Kommentar. go test führt sie aus und schlägt fehl, wenn die Ausgabe abweicht, und go doc und pkg.go.dev zeigen sie als Dokumentation:
// example_test.go
package textutil_test
import (
"fmt"
"example.com/textutil"
)
func ExampleSlugify() {
fmt.Println(textutil.Slugify("Hello, World!"))
// Output: hello-world
}
Der Paketname textutil_test macht das zu einem externen Test: Er kann nur die exportierte API nutzen, wie ein echter Aufrufer. Solche Dateien dürfen im selben Verzeichnis wie das Paket liegen. Ohne einen Kommentar // Output: wird das Example kompiliert, aber nicht ausgeführt. Nimm // Unordered output:, wenn Zeilen in beliebiger Reihenfolge erscheinen können.
Fuzzing in Kürze
Go 1.18 hat Fuzz-Tests eingeführt, die Eingaben erzeugen, um Abstürze und verletzte Invarianten zu finden:
func FuzzSlugify(f *testing.F) {
f.Add("Hello, World!") // seed input
f.Fuzz(func(t *testing.T, s string) {
slug := Slugify(s)
if strings.Contains(slug, "--") || strings.HasPrefix(slug, "-") {
t.Errorf("Slugify(%q) = %q: bad hyphens", s, slug)
}
})
}
Ein schlichtes go test führt nur die Seed-Eingaben aus (die Werte aus f.Add und alle gespeicherten Dateien unter testdata/fuzz). go test -fuzz=FuzzSlugify erzeugt immer neue, bis es einen Fehlschlag findet oder du es stoppst, und speichert fehlschlagende Eingaben unter testdata/fuzz, damit sie zu normalen Testfällen werden.
Häufige Fehler
- Testdatei am falschen Ort oder falsch benannt. Sie muss auf
_test.goenden und im Paketverzeichnis liegen.slug_tests.gowird ins Paket kompiliert, nicht als Test ausgeführt. - Kleinbuchstabe nach
Test.Testslugifyist kein Test.TestSlugifyundTest_slugifysind es. t.Fatalaus einer anderen Goroutine. Melde dort mitt.Errorund warte auf die Goroutine, bevor der Test zurückkehrt.- Meldungen ohne die Eingabe. Nenne, was hineinging, was herauskam und was erwartet wurde.
- Einem gecachten Bestehen vertrauen. Nimm
-count=1, wenn ein Test von etwas außerhalb des Pakets abhängt. - Tests, die von der Reihenfolge anderer Tests oder von geteilten globalen Variablen abhängen. Jeder Test sollte seinen eigenen Zustand aufbauen, und genau dafür gibt es
t.TempDir,t.Setenvundt.Cleanup.
Häufig gestellte Fragen
Wie schreibe ich in Go einen Unit-Test?
Leg im selben Verzeichnis wie der Code eine Datei an, deren Name auf _test.go endet, importiere testing und schreib eine Funktion func TestName(t *testing.T), die deinen Code aufruft und Abweichungen mit t.Errorf meldet. Führ go test in diesem Verzeichnis aus oder go test ./... für das ganze Modul. Ein Framework oder eine Assertion-Bibliothek brauchst du nicht.
Was ist der Unterschied zwischen t.Error und t.Fatal in Go?
t.Error und t.Errorf markieren den Test als fehlgeschlagen und lassen ihn weiterlaufen, sodass ein Lauf mehrere Probleme melden kann. t.Fatal und t.Fatalf markieren ihn als fehlgeschlagen und stoppen den Test sofort. Nimm Fatal, wenn Weitermachen keinen Sinn ergibt, etwa nach einem unerwarteten Fehler, der das Ergebnis unbrauchbar macht.
Wie führe ich in Go einen einzelnen Test aus?
Übergib -run einen regulären Ausdruck: go test -run TestSlugify führt jeden Test aus, dessen Name passt, und go test -run 'TestSlugifyTable/punctuation' führt einen einzelnen Subtest aus (Leerzeichen in Subtest-Namen werden zu Unterstrichen). Füg -v hinzu, um Namen und Ergebnis jedes Tests zu sehen, und -count=1, um den Test-Cache zu umgehen.
Wie schreibe ich in Go einen Benchmark?
Schreib func BenchmarkName(b *testing.B) in eine _test.go-Datei und setz den zu messenden Code in eine Schleife for b.Loop() { ... } (Go 1.24; älterer Code nutzt for i := 0; i < b.N; i++). Führ ihn mit go test -bench=. -benchmem aus, das Nanosekunden, Bytes und Allokationen pro Operation meldet.
Wie sehe ich in Go die Testabdeckung?
go test -cover gibt den Prozentsatz der Anweisungen aus, die die Tests ausgeführt haben. Für Details schreibst du mit go test -coverprofile=cover.out ein Profil und führst dann go tool cover -func=cover.out für Zahlen pro Funktion aus oder go tool cover -html=cover.out, um abgedeckte und nicht abgedeckte Zeilen im Browser zu sehen.