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.
Intisari
httptest.NewRequest+httptest.NewRecordermenguji handler tanpa jaringan.- Uji lewat router lengkap, bukan handler telanjang — supaya middleware dan otorisasi ikut teruji.
httptest.NewServermembuat 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 diuji | Cara membuatnya |
|---|---|
| Timeout | Handler yang menunggu r.Context().Done() |
| 500 lalu 200 | Penghitung di dalam handler |
| Respons rusak | Tulis JSON yang tidak lengkap |
| Respons raksasa | Tulis 10 MB; buktikan LimitReader-mu bekerja |
429 dengan Retry-After | Set header dan status |
| TLS | httptest.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.