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

httptest — menguji handler & klien

httptest membuat tes HTTP di Go murah: satu permintaan palsu, satu perekam respons, tanpa port dan tanpa menunggu. Itulah alasan tes tingkat handler layak ditulis untuk hampir setiap endpoint.

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

Intisari

  • httptest.NewRequest + httptest.NewRecorder menguji handler tanpa jaringan.
  • Uji lewat router lengkap, bukan handler telanjang — supaya middleware dan otorisasi ikut teruji.
  • httptest.NewServer membuat server sungguhan untuk menguji klien HTTP-mu.
  • Periksa status, header penting, dan bentuk body — API yang berubah diam-diam adalah kerusakan bagi konsumennya.
  • Tes otorisasi tingkat HTTP adalah tempat terbaik menangkap kebocoran objek antar pengguna.

Menguji satu handler

func TestAmbilProduk(t *testing.T) {
	h := New(layananUji(t), slog.Default())

	req := httptest.NewRequest(http.MethodGet, "/api/v1/produk/1", nil)
	rec := httptest.NewRecorder()

	// Lewat ROUTER, bukan h.AmbilProduk(rec, req) langsung — supaya
	// middleware, parameter jalur, dan otorisasi ikut teruji.
	h.Rute().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, mau 200; body: %s", rec.Code, rec.Body)
	}
	if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
		t.Errorf("Content-Type = %q", ct)
	}

	var got produkResp
	if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
		t.Fatalf("body bukan JSON valid: %v", err)
	}
	if got.Nama != "Kopi Arabika" {
		t.Errorf("nama = %q, mau %q", got.Nama, "Kopi Arabika")
	}
}

Cetak rec.Body di pesan kegagalan status. Saat tes gagal dengan 500, isi body biasanya sudah memuat pesan error yang menjelaskan sebabnya — tanpa itu kamu harus menjalankan ulang dengan logging tambahan.

Permintaan dengan body dan autentikasi

func TestBuatProduk(t *testing.T) {
	h := New(layananUji(t), slog.Default())

	body := strings.NewReader(`{"nama":"Kopi Robusta","harga":45000}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/produk", body)
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "sesi", Value: sesiUji(t, 1)})

	rec := httptest.NewRecorder()
	h.Rute().ServeHTTP(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("status = %d, mau 201; body: %s", rec.Code, rec.Body)
	}
	if loc := rec.Header().Get("Location"); loc == "" {
		t.Error("header Location kosong")
	}
}

Tes otorisasi — yang paling penting

func TestPenggunaTidakBisaMembacaMilikOrangLain(t *testing.T) {
	h := New(layananUji(t), slog.Default())

	// Faktur 100 milik pengguna 1.
	req := httptest.NewRequest(http.MethodGet, "/api/v1/faktur/100", nil)
	req.AddCookie(&http.Cookie{Name: "sesi", Value: sesiUji(t, 2)})   // pengguna 2

	rec := httptest.NewRecorder()
	h.Rute().ServeHTTP(rec, req)

	if rec.Code != http.StatusNotFound {
		t.Fatalf("pengguna lain dapat status %d, mau 404; body: %s",
			rec.Code, rec.Body)
	}
}

func TestDaftarHanyaMilikSendiri(t *testing.T) {
	// Yang paling sering terlewat: endpoint DETAIL sudah aman,
	// endpoint DAFTAR masih mengembalikan semua baris.
	req := httptest.NewRequest(http.MethodGet, "/api/v1/faktur", nil)
	req.AddCookie(&http.Cookie{Name: "sesi", Value: sesiUji(t, 2)})

	rec := httptest.NewRecorder()
	h.Rute().ServeHTTP(rec, req)

	var hasil struct{ Data []fakturResp }
	_ = json.Unmarshal(rec.Body.Bytes(), &hasil)

	for _, f := range hasil.Data {
		if f.PenggunaID != 2 {
			t.Fatalf("daftar memuat faktur milik pengguna %d", f.PenggunaID)
		}
	}
}

Dua tes di atas adalah tes paling berharga di seluruh suite-mu. Mereka menutup kelas kerentanan yang paling sering ditemukan di aplikasi web nyata (Fase 6), dan biayanya belasan baris. Tulis pasangan ini untuk setiap sumber daya yang punya pemilik.

Menguji klien HTTP

func TestKlienPembayaran(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) {
			// Periksa juga apa yang DIKIRIM klienmu
			if got := r.Header.Get("Authorization"); got != "Bearer rahasia" {
				t.Errorf("Authorization = %q", got)
			}
			if r.URL.Path != "/pembayaran/abc" {
				t.Errorf("path = %q", r.URL.Path)
			}
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{"status":"lunas","jumlah":45000}`))
		}))
	t.Cleanup(srv.Close)

	k := NewKlienPembayaran(srv.URL, srv.Client(), "rahasia")

	s, err := k.Status(t.Context(), "abc")
	if err != nil {
		t.Fatalf("Status() error = %v", err)
	}
	if s.Status != "lunas" {
		t.Errorf("status = %q, mau %q", s.Status, "lunas")
	}
}

Menguji jalur kegagalan

func TestKlienSaatTimeout(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) {
			<-r.Context().Done()      // tidak pernah menjawab
		}))
	t.Cleanup(srv.Close)

	k := NewKlienPembayaran(srv.URL, srv.Client(), "rahasia")

	ctx, batal := context.WithTimeout(t.Context(), 100*time.Millisecond)
	defer batal()

	if _, err := k.Status(ctx, "abc"); !errors.Is(err, context.DeadlineExceeded) {
		t.Errorf("err = %v, mau DeadlineExceeded", err)
	}
}
Skenario yang layak diujiCara membuatnya
TimeoutHandler yang menunggu r.Context().Done()
500 lalu 200Penghitung di dalam handler
Respons rusakTulis JSON yang tidak lengkap
Respons raksasaTulis 10 MB; buktikan LimitReader-mu bekerja
429 dengan Retry-AfterSet header dan status
TLShttptest.NewTLSServer + srv.Client()

Menghitung kueri per permintaan

// Pagar terhadap N+1 (Fase 4): kalau jumlah kueri naik seiring
// jumlah baris, tesnya gagal.
func TestDaftarTidakNPlus1(t *testing.T) {
	db, hitung := dbBerhitung(t)
	h := New(layananDengan(db), slog.Default())

	buatProduk(t, db, 50)

	hitung.Reset()
	req := httptest.NewRequest(http.MethodGet, "/api/v1/produk?batas=50", nil)
	h.Rute().ServeHTTP(httptest.NewRecorder(), req)

	if n := hitung.Jumlah(); n > 3 {
		t.Errorf("daftar 50 produk memakai %d kueri, mau ≤ 3", n)
	}
}

Latihan: tulis tes handler yang melewati router lengkap untuk satu endpoint GET dan satu POST. Tambahkan pasangan tes otorisasi (404 untuk objek milik orang lain, dan daftar yang tidak bocor). Lalu sengaja hapus pembatasan kepemilikan di kuerimu dan pastikan tesnya gagal — tes yang tidak pernah gagal tidak membuktikan apa-apa.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.