Go Testing Httptest Subtests Benchmarks Mocks Fakes
Definisi
Di Go, testing adalah cara utama untuk memverifikasi perilaku kode tanpa harus menjalankan aplikasi secara manual setiap kali ada perubahan. Testing dipakai untuk menjaga kontrak perilaku, menangkap regresi, dan membuat refactor lebih aman.
Topik ini biasanya mencakup beberapa pola inti:
table-driven tests: satu fungsi test menjalankan banyak skenario dari tabel data
testing.T: objek dasar untuk menandai test gagal, log, dan menandai helper
subtests: membagi satu test besar menjadi skenario-skenario kecil dengan t.Run
httptest: paket standar untuk menguji handler HTTP tanpa server sungguhan
mocks/fakes sederhana: pengganti dependency agar test fokus pada perilaku yang diuji
benchmark: pengukuran dasar performa dengan testing.B
Kalau project Go mulai punya banyak branch perilaku, test bukan lagi bonus. Test jadi spesifikasi hidup untuk logika yang penting.
Kapan testing dipakai
Testing paling berguna ketika kamu ingin jawaban cepat untuk pertanyaan seperti:
apakah fungsi ini masih mengembalikan hasil yang benar setelah refactor?
apakah handler HTTP masih membalas status code dan body yang benar?
apakah error branch, timeout, atau fallback masih bekerja?
apakah dependency eksternal bisa diganti supaya test tidak rapuh?
apakah perubahan kecil memperlambat proses secara tidak sengaja?
Secara praktis, testing dipakai saat:
logika mulai punya banyak cabang
ada risiko regresi setelah perubahan
dependency luar tidak stabil atau terlalu mahal dipakai langsung
perilaku penting lebih mahal dicek manual berulang kali
tim butuh spesifikasi yang bisa dibaca ulang oleh manusia dan mesin
Area yang paling terasa manfaatnya
transformasi data
validasi input
parsing dan formatting
service layer yang punya aturan bisnis
HTTP handler dan middleware
repository yang dibungkus dengan interface
code path error, retry, dan timeout
Kalau bug yang sama muncul dua kali, biasanya sudah waktunya menulis test untuk bug itu. Test yang baik mengubah insiden menjadi spesifikasi.
Struktur test yang sehat
Struktur test yang sehat di Go biasanya punya pola yang sederhana dan konsisten.
Prinsip dasar
satu test fokus pada satu perilaku utama
nama test menjelaskan skenario, bukan implementasi internal
input, aksi, dan verifikasi terlihat jelas
setup test tidak terlalu banyak copy-paste
dependency eksternal dikontrol atau diganti
assertion gagal dengan pesan yang cukup jelas
Pola AAA
Banyak test sehat mengikuti pola Arrange - Act - Assert:
Arrange: siapkan input, dependency, dan state awal
Act: jalankan fungsi atau handler
Assert: periksa hasil, error, side effect, atau output
Pola ini membantu test dibaca cepat. Kalau test mulai terlihat campur aduk, biasanya langkah-langkahnya belum dipisah dengan rapi.
Hal yang sebaiknya dijaga
gunakan data test yang eksplisit, bukan angka atau string misterius
jangan membuat test bergantung pada urutan test lain
hindari state global yang bocor antar test
beri helper kecil untuk setup yang berulang
pakai t.Helper() pada helper assertion agar stack trace lebih akurat
testing.T: fondasi test di Go
testing.T adalah objek yang dipakai hampir semua test unit di Go. Lewat *testing.T, kamu bisa menandai test gagal, mencatat log, menjalankan cleanup, dan menandai helper function.
Fungsi yang sering dipakai
t.Error / t.Errorf: menandai gagal tapi test tetap lanjut
t.Fatal / t.Fatalf: menandai gagal dan menghentikan test saat itu juga
t.Helper(): menandai helper supaya lokasi error lebih berguna
t.Cleanup(fn): menjalankan cleanup setelah test selesai
t.Run(name, fn): menjalankan subtest
Kapan pakai Fatal dan kapan pakai Error
Pakai t.Fatal atau t.Fatalf kalau test tidak masuk akal untuk dilanjutkan setelah gagal. Contoh:
setup gagal
dependency penting tidak terbentuk
response yang diuji tidak valid sejak awal
Pakai t.Error atau t.Errorf kalau kamu ingin mengumpulkan beberapa kegagalan dalam satu test. Untuk kebanyakan test perilaku, Fatalf sering lebih jelas karena kegagalan pertama sudah cukup untuk diagnosis.
Helper() penting supaya failure line menunjuk ke caller, bukan ke dalam helper itu sendiri.
Table-driven tests
Table-driven test adalah pola default yang sangat umum di Go. Satu fungsi test menjalankan banyak skenario dari tabel data.
Kenapa pola ini dipakai
test lebih ringkas
skenario baru gampang ditambah
perbandingan input-output terlihat jelas
konsisten untuk fungsi yang punya banyak cabang
mudah digabung dengan subtest
Bentuk dasar
go
func TestSlugify(t *testing.T) { tests := []struct { name string in string want string }{ {name: "trim spaces", in: " Halo Dunia ", want:
Kenapa cocok digabung dengan subtests
Setiap baris tabel bisa jadi subtest sendiri. Itu membuat output test lebih mudah dipindai. Kalau satu skenario gagal, kamu langsung tahu skenario mana yang rusak.
Jebakan umum pada table-driven test
loop variable ditangkap closure tanpa dibuat lokal ulang
nama case tidak deskriptif
assertion terlalu generik sehingga sulit debug
semua skenario digabung tanpa subtest, lalu output gagal jadi sulit dibaca
tabel berisi banyak setup yang menutupi maksud test
Catatan penting soal closure
Kalau memakai t.Run di loop, biasakan membuat salinan lokal dari variabel loop ketika perlu. Ini mencegah masalah capture yang membingungkan.
go
for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { // pakai tt di sini })}
Subtests dengan t.Run
Subtests dipakai untuk memecah satu test besar menjadi beberapa skenario yang lebih spesifik. Ini berguna saat satu fungsi punya banyak branch atau saat kamu ingin mengelompokkan kasus berdasarkan kategori.
Kapan subtests dipakai
satu test punya banyak case
ingin menjalankan satu skenario tertentu lewat go test -run
ingin set up dan tear down yang beda per skenario
ingin hierarki test yang lebih jelas
Contoh subtest bertingkat
go
func TestParseRole(t *testing.T) { tests := []struct { name string in string want Role }{ {name: "admin", in: "admin", want: RoleAdmin},
Manfaat praktis subtests
output lebih rapi
case yang gagal lebih mudah diisolasi
satu subset case bisa dijalankan sendiri
setup khusus per case jadi mungkin tanpa mengotori case lain
Jebakan umum subtests
terlalu banyak nesting sehingga output sulit dibaca
nama subtest tidak konsisten
subtest bergantung pada state yang diubah subtest lain
cleanup tidak dipisah per subtest
Alur execution test
Mermaid membantu untuk melihat urutan eksekusi test, terutama saat memakai table-driven test dan subtests.
Loading diagram...
httptest untuk HTTP testing
Paket net/http/httptest dipakai untuk menguji handler HTTP tanpa perlu menjalankan server sungguhan.
Komponen yang sering dipakai
httptest.NewRequest: membuat request palsu untuk test
httptest.NewRecorder: merekam response handler
httptest.NewServer: menjalankan server test sungguhan di port acak
httptest.ResponseRecorder: penampung hasil response
Kapan httptest dipakai
menguji handler HTTP secara langsung
memeriksa status code, header, dan body
memastikan routing atau middleware menghasilkan response yang benar
menguji flow request tanpa network call nyata
Contoh handler sederhana
go
func HelloHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type"
Pakai httptest.NewServer kalau kamu perlu menguji perilaku client atau middleware stack yang lebih realistis, misalnya saat ada redirect, cookie, transport, atau interaksi HTTP penuh.
go
func TestClientAgainstServer(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(HelloHandler)) defer server.Close() resp, err := http.Get(server.URL) if err !=
Jebakan umum HTTP testing
lupa memeriksa status code
hanya mengecek body tanpa header atau method
test terlalu tergantung pada text response yang sangat spesifik
memakai server sungguhan padahal cukup NewRecorder
tidak menutup body response
tidak memisahkan test handler, middleware, dan client concern
Mocks dan fakes sederhana
Di Go, dependency sering dipisah lewat interface lalu diganti dengan fake sederhana saat test. Untuk banyak kasus, fake kecil lebih jujur dan lebih mudah dirawat daripada mock yang terlalu agresif.
Perbedaan singkat
mock: objek kontrol yang biasanya memverifikasi interaksi tertentu
fake: implementasi sederhana yang punya perilaku cukup untuk test
stub: dependency yang hanya mengembalikan data tertentu
Contoh interface
go
type UserStore interface { FindByID(id int) (User, error)}
Fake sederhana
go
type FakeUserStore struct { Users map[int]User}func (f FakeUserStore) FindByID(id int) (User, error) { user, ok := f.Users[id] if !
Contoh penggunaan di service
go
type UserService struct { Store UserStore}func (s UserService) GetName(id int) (string, error) { user, err := s.Store.FindByID(id) if err != nil
yang diuji adalah hasil, bukan urutan panggilan internal
kamu ingin test tetap mudah dipahami 6 bulan lagi
Kapan mock lebih masuk akal
interaksi spesifik memang bagian dari kontrak
ada side effect yang harus diverifikasi
kamu perlu memastikan call count atau urutan panggilan
Jebakan umum mock/fake
mock terlalu detail sampai test mengunci implementasi internal
fake terlalu pintar dan mulai meniru production logic penuh
interface dibuat terlalu besar hanya untuk memudahkan test
test memeriksa hal yang tidak penting bagi perilaku publik
Aturan praktisnya: kalau fake kecil sudah cukup, pakai fake. Kalau kamu benar-benar perlu memverifikasi interaksi, baru naik ke mock.
Benchmark dasar
Benchmark dipakai untuk mengukur performa secara kasar dan membandingkan perubahan. Di Go, benchmark dijalankan lewat fungsi BenchmarkXxx dengan *testing.B.
Kapan benchmark dipakai
ingin tahu apakah refactor mempercepat atau memperlambat kode
ingin membandingkan dua pendekatan implementasi
ingin melihat alokasi memori atau throughput dasar
ingin mengukur path yang sering dipanggil
Contoh benchmark
go
func BenchmarkSlugify(b *testing.B) { for i := 0; i < b.N; i++ { _ = Slugify("Go Testing dan HTTP") }}
Menulis benchmark yang sehat
jangan mengukur setup yang bukan bagian dari operasi utama
siapkan data di luar loop kalau memang tidak ikut diukur
gunakan b.ResetTimer() setelah setup bila perlu
pakai b.ReportAllocs() kalau ingin melihat alokasi
go
func BenchmarkParseJSON(b *testing.B) { data := []byte(`{"name":"Andi","age":30}`) b.ReportAllocs() for i := 0; i < b.N; i++ { var v map
Jebakan umum benchmark
benchmark terlalu kecil sehingga noise besar
membandingkan hasil tanpa menjalankan cukup iterasi
setup ikut terukur tanpa disengaja
membaca angka benchmark sebagai kebenaran mutlak, padahal konteks mesin dan load juga berpengaruh
Benchmark bagus untuk diagnosis performa, bukan untuk menggantikan profiling.
Alur keputusan: pakai test, subtest, atau benchmark?
fixture disimpan di testdata kalau datanya besar atau dipakai ulang
helper test bisa diletakkan di file khusus bila mulai banyak
Kapan memakai testdata
Pakai testdata kalau:
input test cukup besar
kamu perlu file fixture nyata
isi fixture ingin mudah dibaca ulang
lebih nyaman menyimpan contoh payload daripada inline string panjang
Kesalahan umum pada test yang rapuh
Test rapuh biasanya lulus hari ini, lalu gagal karena perubahan kecil yang sebenarnya tidak memengaruhi perilaku penting.
Ciri test rapuh
terlalu bergantung pada detail implementasi internal
terlalu banyak mocking untuk hal yang tidak penting
bergantung pada waktu, urutan, atau random tanpa kontrol
assertion terlalu spesifik pada formatting yang tidak penting
share state antar test
hasil test tergantung environment lokal
Contoh jebakan yang sering muncul
membandingkan error string mentah padahal yang penting jenis error-nya
menunggu time.Sleep tanpa alasan yang jelas
membaca output log sebagai satu-satunya bukti perilaku
test jaringan yang bergantung internet nyata
memakai global variable yang dimodifikasi lintas test
Cara mengurangi kerapuhan
fokus pada kontrak perilaku, bukan detail internal
kontrol clock dan random bila perlu
gunakan helper dan fake sederhana
buat assertion yang relevan dengan user-visible behavior atau contract internal yang benar-benar penting
jalankan test dalam kondisi paralel hanya kalau state memang aman
Catatan implementasi
Beberapa kebiasaan kecil sangat membantu kualitas test:
beri nama test yang menjelaskan kondisi dan hasil
simpan setup yang berulang dalam helper
pakai t.Cleanup untuk resource yang perlu ditutup
gunakan httptest untuk HTTP layer, bukan server sungguhan kalau tidak perlu
pilih fake sederhana dulu sebelum mock kompleks
gunakan benchmark ketika ada pertanyaan performa yang konkret
Referensi ringkas
package testing
package net/http/httptest
package net/http
subtest dengan t.Run
benchmark dengan testing.B
Kalau perlu, halaman berikutnya bisa memperluas topik ini ke test parallel, coverage, race detector, dan integration test untuk service yang lebih besar.