← Semua pembelajaran / Go Nol → Enterprise
Fase 8 · Testing & Kualitas Kode

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.

Sumber asli pkg.go.dev Resmi Rangkuman ~6 menit baca

Intisari

  • Eksperimental di Go 1.24, stabil di Go 1.25 sebagai testing/synctest.
  • Di dalam synctest.Test, time.Sleep dan time.After memakai jam palsu — tes selesai seketika.
  • synctest.Wait menunggu 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")
	}
}
MasalahAkibatnya
LambatSeratus tes seperti ini = suite bermenit-menit
FlakyRunner CI yang sibuk membuat 150 ms tidak cukup
Tidak bisa menguji durasi panjang"Kedaluwarsa setelah 24 jam" mustahil diuji
Menyembunyikan bugSleep 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

HalDi dalam gelembung
WaktuPalsu — time.Now, Sleep, After, Ticker
Goroutine di luar gelembungTidak diikutkan; Wait tidak menunggunya
I/O sungguhan (berkas, jaringan luar)Tetap nyata — bisa membuat gelembung tidak pernah tenang
httptest lokalBekerja; koneksinya di dalam proses
Versi GoButuh 1.25+ (1.24 dengan GOEXPERIMENT=synctest)

Kapan memakainya

  1. Kedaluwarsa cache dan TTL.
  2. Retry dengan backoff.
  3. Timeout dan pembatalan context.
  4. Rate limiter berbasis waktu.
  5. Worker, penjadwal, dan Ticker.
  6. 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.