Kode yang diuji
Alat testing Go adalah bagian dari toolchain standar: package testing dan perintah go test. Tidak ada framework yang perlu diinstal, dan tidak butuh library assertion.
Contoh di halaman ini menguji satu fungsi kecil, Slugify, yang mengubah sebuah judul menjadi slug URL. Ini dia, bisa dijalankan, dengan pemeriksaan buatan sendiri di main:
Editor di browser menjalankan main; editor itu tidak bisa menjalankan go test. Semua yang ada di bawah ditulis seperti di proyek sungguhan, dengan output terminal ditampilkan setelah setiap perintah. Semuanya dijalankan dengan Go 1.24.
Tes pertama
Di sebuah proyek, fungsi berada di dalam package, dan tesnya berada di sebelahnya, di file yang namanya diakhiri _test.go:
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)
}
}
Aturan yang dipakai go test:
- Hanya file yang diakhiri
_test.goyang dianggap file tes.go buildmengabaikannya, jadi kode tes tidak pernah masuk ke binary-mu. - Tes adalah fungsi bernama
TestXxx(bagian setelahTesttidak boleh diawali huruf kecil) yang menerima satu*testing.T. - Tes dianggap lulus kecuali ia memanggil salah satu method kegagalan atau panic.
go test # the package in the current directory
go test ./... # every package in the module
$ go test
PASS
ok example.com/textutil 0.318s
Format pesan kegagalan Func(input) = got, want expected adalah konvensi Go. Masukkan input-nya ke dalam pesan: kegagalan yang hanya bertuliskan got "a", want "b" memaksamu kembali ke kode untuk mencari tahu input mana yang dimaksud.
t.Errorf dibandingkan t.Fatalf
| Method | Menandai gagal | Menghentikan tes |
|---|---|---|
t.Error, t.Errorf | ya | tidak, tetap berjalan |
t.Fatal, t.Fatalf | ya | ya, saat itu juga |
t.Log, t.Logf | tidak | tidak, hanya dicetak dengan -v atau saat gagal |
t.Skip, t.Skipf | tidak | ya, dilaporkan sebagai dilewati |
Pakai Errorf secara default, agar satu run melaporkan semua field yang salah. Pakai Fatalf ketika sisa tes tidak mungkin berjalan, biasanya setelah error yang tidak terduga:
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 menghentikan tes dengan memanggil runtime.Goexit, jadi ia harus dipanggil dari goroutine milik tes itu sendiri. Dari goroutine yang kamu jalankan sendiri, laporkan dengan t.Error lalu return.
Table-driven test dengan t.Run
Sebagian besar tes Go berbentuk tabel: sebuah slice berisi kasus, satu perulangan, dan t.Run untuk memberi setiap kasus namanya sendiri.
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)
}
})
}
}
Menambah kasus cukup satu baris. Setiap subtest dilaporkan terpisah, Fatal di dalam satu subtest hanya mengakhiri subtest itu, dan kamu bisa menjalankan satu subtest berdasarkan namanya. Jalankan semuanya dengan -v:
$ 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
Spasi di nama subtest berubah menjadi garis bawah. Saat sebuah kasus gagal, output-nya menyebut subtest dan barisnya:
--- 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
Sejak Go 1.22, setiap iterasi perulangan punya tt sendiri, jadi closure di dalam t.Run melihat kasus yang benar walaupun subtest berjalan paralel. Kode lama sering menulis tt := tt di awal badan perulangan karena alasan itu; sekarang hal itu tidak dibutuhkan lagi.
Untuk menjalankan subtest secara paralel, panggil t.Parallel() di awal fungsi subtest. Lakukan ini hanya kalau kasus-kasusnya independen dan cukup lambat sehingga hasilnya terasa.
Flag go test yang akan kamu pakai
| Perintah | Fungsinya |
|---|---|
go test -v | mencetak nama, hasil, dan output t.Log setiap tes |
go test -run TestSlugify | menjalankan tes yang namanya cocok dengan regular expression |
go test -run 'TestSlugifyTable/empty' | menjalankan satu subtest |
go test -count=1 | mengabaikan hasil di cache dan menjalankan ulang |
go test -race | menjalankan dengan data race detector |
go test -short | meminta tes yang lama untuk melewati dirinya sendiri (if testing.Short() { t.Skip() }) |
go test -failfast | berhenti setelah tes pertama yang gagal |
go test -timeout 30s | gagal jika run berlangsung lebih lama (default 10 menit) |
go test -cover | mencetak coverage pernyataan |
go test -bench=. | ikut menjalankan benchmark |
Saat kamu menyebut package (go test ., go test ./...), go test menyimpan hasil di cache untuk package yang kode dan input-nya tidak berubah, lalu mencetak (cached) di belakangnya. go test biasa tanpa argumen tidak pernah memakai cache. Cache melacak file dan variabel lingkungan yang dibaca tes lewat package os, tetapi tidak melacak keadaan di luar seperti database atau layanan jaringan, jadi tes yang bergantung pada hal itu bisa lulus dari cache padahal sekarang akan gagal; -count=1 memaksa run yang sungguhan.
Helper, direktori sementara, dan cleanup
Panggilan writeFile di TestCountWords di atas adalah sebuah test helper:
// 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()menandai fungsi itu sebagai helper, jadi kegagalan dilaporkan di baris tes yang memanggilnya, bukan di dalam helper.t.TempDir()membuat direktori baru dan menghapusnya saat tes selesai. Tes tidak pernah perlu membersihkan file sendiri.t.Cleanup(func() { ... })mendaftarkan proses pembongkaran lainnya (menutup server, menghapus tabel). Cleanup dijalankan setelah tes dan subtest-nya, yang terakhir didaftarkan dijalankan lebih dulu.t.Setenv("KEY", "value")mengatur variabel lingkungan hanya untuk tes ini dan mengembalikannya setelah itu.t.Context()(Go 1.24) mengembalikan context yang dibatalkan tepat sebelum cleanup dijalankan.
File fixture tes ditaruh di direktori bernama testdata di sebelah tesnya. Tool go tidak menganggapnya sebagai package, dan tes berjalan dengan direktori package sebagai direktori kerja, jadi os.ReadFile("testdata/input.json") bisa dipakai.
Menguji error dan HTTP handler
Periksa error dengan errors.Is atau errors.As, sama seperti yang dilakukan kode pemanggilnya:
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)
}
}
Membandingkan string error akan rusak begitu seseorang mengubah kata-kata pesannya.
Untuk HTTP handler, net/http/httptest memberimu ResponseWriter palsu sehingga kamu bisa memanggil handler secara langsung, tanpa jaringan:
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 menjalankan server sungguhan di localhost untuk menguji kode client. Halaman HTTP client memakainya di setiap contoh.
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 memberi tahu baris mana yang tidak pernah dijalankan, yang berguna untuk menemukan cabang yang belum diuji. Angka yang tinggi tidak berarti assertion-nya bagus: tes yang memanggil setiap fungsi tanpa memeriksa apa pun tetap mencapai 100%.
Benchmark
Benchmark adalah func BenchmarkXxx(b *testing.B). Go 1.24 menambahkan b.Loop, yang sekarang menjadi bentuk yang direkomendasikan:
func BenchmarkSlugify(b *testing.B) {
for b.Loop() {
Slugify("The Go Programming Language, 2nd Edition")
}
}
b.Loop menjalankan badan perulangan sebanyak yang diperlukan untuk pengukuran yang stabil, tidak menghitung kode setup sebelum perulangan dalam waktu pengukuran, dan mencegah compiler mengoptimalkan panggilan itu sampai hilang. Kode yang ditulis sebelum Go 1.24 memakai for i := 0; i < b.N; i++, yang masih bisa dipakai tetapi butuh b.ResetTimer() setelah setup yang mahal dan bisa tertipu oleh dead-code elimination.
Benchmark tidak dijalankan oleh go test biasa. Mintalah secara eksplisit, dan lewati tes biasa dengan -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
Kolomnya adalah: nama dengan tambahan GOMAXPROCS, jumlah iterasi yang dijalankan, waktu per panggilan, byte yang dialokasikan per panggilan, dan jumlah alokasi per panggilan. Angkanya bergantung pada mesin; bandingkan run di mesin yang sama. Untuk membandingkan sebelum dan sesudah perubahan dengan andal, jalankan setiap sisi beberapa kali dengan -count=10 dan berikan kedua output-nya ke benchstat (golang.org/x/perf/cmd/benchstat).
Example juga merupakan tes
Fungsi example mencetak sesuatu dan menyatakan output yang diharapkan di sebuah komentar. go test menjalankannya dan gagal kalau output-nya berbeda, sedangkan go doc dan pkg.go.dev menampilkannya sebagai dokumentasi:
// example_test.go
package textutil_test
import (
"fmt"
"example.com/textutil"
)
func ExampleSlugify() {
fmt.Println(textutil.Slugify("Hello, World!"))
// Output: hello-world
}
Nama package textutil_test menjadikannya tes eksternal: tes ini hanya bisa memakai API yang diekspor, seperti pemanggil sungguhan. File seperti ini boleh berada di direktori yang sama dengan package-nya. Tanpa komentar // Output:, example dikompilasi tetapi tidak dijalankan. Pakai // Unordered output: kalau baris-barisnya bisa muncul dalam urutan apa pun.
Sekilas tentang fuzzing
Go 1.18 menambahkan fuzz test, yang menghasilkan input untuk menemukan crash dan invarian yang rusak:
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)
}
})
}
go test biasa hanya menjalankan input seed (nilai dari f.Add dan file yang tersimpan di testdata/fuzz). go test -fuzz=FuzzSlugify terus menghasilkan input baru sampai menemukan kegagalan atau kamu menghentikannya, lalu menyimpan input yang gagal di testdata/fuzz sehingga menjadi kasus uji biasa.
Kesalahan umum
- File tes di tempat yang salah atau namanya salah. Namanya harus diakhiri
_test.godan file-nya berada di direktori package.slug_tests.godikompilasi ke dalam package, bukan dijalankan sebagai tes. - Huruf kecil setelah
Test.Testslugifybukan tes.TestSlugifydanTest_slugifyadalah tes. t.Fataldari goroutine lain. Di sana, laporkan dengant.Error, dan tunggu goroutine itu selesai sebelum tes return.- Pesan tanpa input. Sertakan apa yang dimasukkan, apa yang keluar, dan apa yang diharapkan.
- Memercayai hasil lulus dari cache. Pakai
-count=1kalau tes bergantung pada apa pun di luar package. - Tes yang bergantung pada urutan tes lain atau variabel global bersama. Setiap tes sebaiknya menyiapkan keadaannya sendiri, dan untuk itulah
t.TempDir,t.Setenv, dant.Cleanupada.
Pertanyaan yang sering diajukan
Bagaimana cara menulis unit test di Go?
Buat file yang namanya diakhiri _test.go di direktori yang sama dengan kodenya, import testing, lalu tulis fungsi func TestName(t *testing.T) yang memanggil kodemu dan melaporkan ketidakcocokan dengan t.Errorf. Jalankan go test di direktori itu, atau go test ./... untuk seluruh module. Tidak perlu framework atau library assertion.
Apa bedanya t.Error dan t.Fatal di Go?
t.Error dan t.Errorf menandai tes sebagai gagal dan tetap melanjutkannya, jadi satu run bisa melaporkan beberapa masalah. t.Fatal dan t.Fatalf menandainya gagal dan langsung menghentikan tes. Pakai Fatal ketika melanjutkan tes tidak ada gunanya, misalnya setelah error tak terduga yang membuat hasilnya tidak bisa dipakai.
Bagaimana cara menjalankan satu tes saja di Go?
Berikan regular expression ke -run: go test -run TestSlugify menjalankan setiap tes yang namanya cocok, dan go test -run 'TestSlugifyTable/punctuation' menjalankan satu subtest (spasi di nama subtest menjadi garis bawah). Tambahkan -v untuk melihat nama dan hasil setiap tes, dan -count=1 untuk melewati cache tes.
Bagaimana cara menulis benchmark di Go?
Tulis func BenchmarkName(b *testing.B) di file _test.go dan taruh kode yang ingin diukur di dalam loop for b.Loop() { ... } (Go 1.24; kode lama memakai for i := 0; i < b.N; i++). Jalankan dengan go test -bench=. -benchmem, yang melaporkan nanodetik, byte, dan alokasi per operasi.
Bagaimana cara melihat coverage tes di Go?
go test -cover mencetak persentase pernyataan yang dijalankan oleh tes. Untuk detailnya, tulis profil dengan go test -coverprofile=cover.out, lalu jalankan go tool cover -func=cover.out untuk angka per fungsi, atau go tool cover -html=cover.out untuk melihat baris yang tercakup dan yang tidak di browser.