testing/synctest — menguji concurrency
Tes yang memakai time.Sleep untuk menunggu goroutine selalu berakhir sama: lambat, dan flaky di CI. synctest menjalankan tes dengan jam palsu yang melompat sendiri saat semua goroutine tertidur.
Intisari
- Eksperimental di Go 1.24, stabil di Go 1.25 sebagai
testing/synctest. - Di dalam
synctest.Test,time.Sleepdantime.Aftermemakai jam palsu — tes selesai seketika. synctest.Waitmenunggu sampai semua goroutine dalam gelembung tes tertidur atau selesai.- Kalau semua goroutine tertidur permanen, tes gagal dengan pesan deadlock — bukan menggantung sampai timeout.
- Tes timeout, retry berjeda, cache TTL, dan rate limiter jadi deterministik dan selesai dalam milidetik.
Masalah yang diselesaikannya
// ❌ Tes tradisional untuk kode berbasis waktu
func TestCacheKedaluwarsa(t *testing.T) {
c := NewCache(100 * time.Millisecond)
c.Simpan("k", "v")
time.Sleep(150 * time.Millisecond) // menebak; lambat; flaky
if _, ok := c.Ambil("k"); ok {
t.Error("mau kedaluwarsa")
}
}
| Masalah | Akibatnya |
|---|---|
| Lambat | Seratus tes seperti ini = suite bermenit-menit |
| Flaky | Runner CI yang sibuk membuat 150 ms tidak cukup |
| Tidak bisa menguji durasi panjang | "Kedaluwarsa setelah 24 jam" mustahil diuji |
| Menyembunyikan bug | Sleep yang kebetulan cukup menutupi balapan sungguhan |
Dengan synctest
import "testing/synctest"
func TestCacheKedaluwarsa(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
c := NewCache(time.Hour)
c.Simpan("k", "v")
// Jam palsu: ini TIDAK menunggu satu jam sungguhan.
// Waktu melompat begitu semua goroutine tertidur.
time.Sleep(2 * time.Hour)
if _, ok := c.Ambil("k"); ok {
t.Error("mau kedaluwarsa setelah 2 jam")
}
})
}
ok contoh.com/toko/internal/cache 0.004s
Empat milidetik untuk menguji perilaku dua jam. Di dalam gelembung synctest, seluruh goroutine memakai jam yang sama dan waktunya hanya maju saat semuanya sedang menunggu. Tidak ada penungguan sungguhan, dan hasilnya deterministik — tes yang sama selalu memberi hasil yang sama.
Menunggu goroutine, bukan menunggu waktu
func TestWorkerMemprosesSemua(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
antrean := make(chan Tugas, 10)
selesai := &atomic.Int64{}
w := NewWorker(antrean, selesai)
go w.Jalankan(t.Context())
for range 10 {
antrean <- Tugas{}
}
// Menunggu SAMPAI semua goroutine di gelembung ini tertidur
// atau selesai — bukan menunggu durasi tertentu.
synctest.Wait()
if n := selesai.Load(); n != 10 {
t.Errorf("selesai = %d, mau 10", n)
}
})
}
Menguji timeout dan retry
func TestRetryDenganBackoff(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var percobaan int
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
percobaan++
if percobaan < 3 {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer srv.Close()
k := NewKlien(srv.URL, srv.Client())
// Backoff-nya 1 dtk, 2 dtk, 4 dtk — dan tesnya tetap selesai
// dalam milidetik.
if err := k.Panggil(t.Context()); err != nil {
t.Fatalf("Panggil() error = %v", err)
}
if percobaan != 3 {
t.Errorf("percobaan = %d, mau 3", percobaan)
}
})
}
Deteksi kebuntuan
func TestGoroutineBocor(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ch := make(chan int)
go func() {
ch <- 1 // tidak ada yang membaca
}()
synctest.Wait()
// Tes GAGAL dengan pesan bahwa semua goroutine tertidur —
// bukan menggantung sampai timeout `go test`.
})
}
Ini melengkapi detektor race dari Fase 2. -race menemukan akses memori yang
bertabrakan; synctest menemukan goroutine yang tidak pernah selesai. Keduanya menutup sisi yang
berbeda dari bug concurrency, dan keduanya layak berjalan di CI.
Batasan
| Hal | Di dalam gelembung |
|---|---|
| Waktu | Palsu — time.Now, Sleep, After, Ticker |
| Goroutine di luar gelembung | Tidak diikutkan; Wait tidak menunggunya |
| I/O sungguhan (berkas, jaringan luar) | Tetap nyata — bisa membuat gelembung tidak pernah tenang |
httptest lokal | Bekerja; koneksinya di dalam proses |
| Versi Go | Butuh 1.25+ (1.24 dengan GOEXPERIMENT=synctest) |
Kapan memakainya
- Kedaluwarsa cache dan TTL.
- Retry dengan backoff.
- Timeout dan pembatalan context.
- Rate limiter berbasis waktu.
- Worker, penjadwal, dan
Ticker. - Graceful shutdown — memastikan semuanya benar-benar berhenti.
Latihan: ambil satu tes yang memakai time.Sleep dan ubah ke
synctest.Test. Bandingkan durasinya dengan go test -v. Lalu tulis tes yang
membuktikan cache-mu kedaluwarsa setelah 24 jam — sesuatu yang tidak mungkin dilakukan
sebelumnya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.