Uji Berbasis Tabel Paralel dalam Go
Percepat tes Go dengan eksekusi paralel
Table-driven tests adalah pendekatan idiomatic Go untuk menguji beberapa skenario secara efisien.
Ketika dikombinasikan dengan eksekusi paralel menggunakan t.Parallel(), Anda dapat secara dramatis mengurangi waktu eksekusi suite pengujian, terutama untuk operasi I/O-bound.
Namun, pengujian paralel memperkenalkan tantangan unik seputar kondisi race dan isolasi pengujian yang memerlukan perhatian yang hati-hati.

Memahami Eksekusi Pengujian Paralel
Go’s package pengujian menyediakan dukungan bawaan untuk eksekusi pengujian paralel melalui metode t.Parallel(). Ketika sebuah pengujian memanggil t.Parallel(), ini memberi sinyal ke runner pengujian bahwa pengujian ini dapat aman dijalankan secara bersamaan dengan pengujian paralel lainnya. Ini sangat kuat ketika dikombinasikan dengan pengujian berbasis tabel, di mana Anda memiliki banyak kasus pengujian independen yang dapat dieksekusi secara bersamaan.
Parallelisme default dikontrol oleh GOMAXPROCS, yang biasanya sama dengan jumlah inti CPU di mesin Anda. Anda dapat menyesuaikan ini dengan flag -parallel: go test -parallel 4 membatasi pengujian paralel menjadi 4, terlepas dari jumlah CPU Anda. Ini berguna untuk mengontrol penggunaan sumber daya atau ketika pengujian memiliki persyaratan konkurensi tertentu.
Untuk pengembang baru di Go pengujian, memahami dasar-dasarnya sangat penting. Panduan kami tentang best practices Go unit testing mencakup pengujian berbasis tabel, subtest, dan dasar-dasar package pengujian yang membentuk fondasi untuk eksekusi paralel.
Pola Dasar Pengujian Berbasis Tabel Paralel
Berikut adalah pola yang benar untuk pengujian berbasis tabel paralel:
func TestCalculate(t *testing.T) {
tests := []struct {
name string
a, b int
op string
expected int
wantErr bool
}{
{"addition", 2, 3, "+", 5, false},
{"subtraction", 5, 3, "-", 2, false},
{"multiplication", 4, 3, "*", 12, false},
{"division", 10, 2, "/", 5, false},
{"division by zero", 10, 0, "/", 0, true},
}
for _, tt := range tests {
tt := tt // Capture loop variable
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Enable parallel execution
result, err := Calculate(tt.a, tt.b, tt.op)
if (err != nil) != tt.wantErr {
t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if result != tt.expected {
t.Errorf("Calculate(%d, %d, %q) = %d; want %d",
tt.a, tt.b, tt.op, result, tt.expected)
}
})
}
}
Baris kritis adalah tt := tt sebelum t.Run(). Ini menangkap nilai saat ini dari variabel loop, memastikan setiap subtest paralel beroperasi pada salinan sendiri dari data kasus pengujian.
Masalah Penangkapan Variabel Loop
Ini adalah salah satu kesalahan paling umum ketika menggunakan t.Parallel() dengan pengujian berbasis tabel. Dalam Go, variabel loop tt dibagikan di seluruh iterasi. Ketika subtest berjalan secara paralel, mereka mungkin semua merujuk pada variabel tt yang sama, yang akan ditimpa seiring berlangsungnya loop. Ini menyebabkan kondisi race dan kegagalan pengujian yang tidak terduga.
Salah (kondisi race):
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Semua subtest mungkin melihat nilai tt yang sama!
result := Calculate(tt.a, tt.b, tt.op)
})
}
Benar (variabel yang ditangkap):
for _, tt := range tests {
tt := tt // Capture the loop variable
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Setiap subtest memiliki salinan sendiri dari tt
result := Calculate(tt.a, tt.b, tt.op)
})
}
Penugasan tt := tt menciptakan variabel baru dengan lingkup iterasi loop, memastikan setiap goroutine memiliki salinan sendiri dari data kasus pengujian.
Memastikan Kemandirian Pengujian
Agar pengujian paralel berjalan dengan benar, setiap pengujian harus sepenuhnya mandiri. Mereka tidak boleh:
- Berbagi state global atau variabel
- Memodifikasi sumber daya bersama tanpa sinkronisasi
- Bergantung pada urutan eksekusi
- Mengakses file, database, atau sumber daya jaringan yang sama tanpa koordinasi
Contoh pengujian paralel mandiri:
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{"valid email", "user@example.com", false},
{"invalid format", "not-an-email", true},
{"missing domain", "user@", true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
tt.email, err, tt.wantErr)
}
})
}
}
Setiap kasus pengujian beroperasi pada data input sendiri tanpa state bersama, membuatnya aman untuk eksekusi paralel.
Mendeteksi Kondisi Race
Go menyediakan detektor race yang kuat untuk menangkap data race dalam pengujian paralel. Selalu jalankan pengujian paralel Anda dengan flag -race selama pengembangan:
go test -race ./...
Detektor race akan melaporkan setiap akses bersamaan ke memori tanpa sinkronisasi yang tepat. Ini sangat penting untuk menangkap bug halus yang mungkin hanya muncul di bawah kondisi timing tertentu.
Contoh kondisi race:
var counter int // State bersama - BERBAHAYA!
func TestIncrement(t *testing.T) {
tests := []struct {
name string
want int
}{
{"test1", 1},
{"test2", 2},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
counter++ // KONDISI RACE!
if counter != tt.want {
t.Errorf("counter = %d, want %d", counter, tt.want)
}
})
}
}
Menjalankan ini dengan -race akan mendeteksi modifikasi bersamaan terhadap counter. Perbaikannya adalah membuat setiap pengujian mandiri dengan menggunakan variabel lokal alih-alih state bersama.
Manfaat Kinerja
Eksekusi paralel dapat secara signifikan mengurangi waktu eksekusi suite pengujian. Kecepatan peningkatan bergantung pada:
- Jumlah inti CPU: Semakin banyak inti, semakin banyak pengujian yang dapat dijalankan secara bersamaan
- Karakteristik pengujian: Pengujian I/O-bound lebih banyak mendapatkan manfaat dibandingkan pengujian CPU-bound
- Jumlah pengujian: Suite pengujian yang lebih besar mendapatkan penghematan waktu yang lebih besar
Mengukur kinerja:
# Eksekusi berurutan
go test -parallel 1 ./...
# Eksekusi paralel (default)
go test ./...
# Paralelisme khusus
go test -parallel 8 ./...
Untuk suite pengujian dengan banyak operasi I/O (query database, permintaan HTTP, operasi file), Anda sering kali dapat mencapai peningkatan kecepatan 2-4x pada sistem multi-core modern. Pengujian CPU-bound mungkin mendapatkan manfaat yang lebih sedikit karena persaingan untuk sumber daya CPU.
Mengontrol Paralelisme
Anda memiliki beberapa opsi untuk mengontrol eksekusi pengujian paralel:
1. Batasi jumlah maksimal pengujian paralel:
go test -parallel 4 ./...
2. Tetapkan GOMAXPROCS:
GOMAXPROCS=2 go test ./...
3. Eksekusi paralel selektif:
Hanya tandai pengujian tertentu dengan t.Parallel(). Pengujian tanpa panggilan ini berjalan secara berurutan, yang berguna ketika beberapa pengujian harus berjalan dalam urutan tertentu atau berbagi sumber daya.
4. Eksekusi paralel bersyarat:
func TestExpensive(t *testing.T) {
if testing.Short() {
t.Skip("melewatkan pengujian mahal dalam mode pendek")
}
t.Parallel()
// Logika pengujian mahal
}
Pola Umum dan Best Practices
Pola 1: Setup Sebelum Eksekusi Paralel
Jika Anda membutuhkan setup yang dibagikan di seluruh kasus pengujian, lakukan sebelum loop:
func TestWithSetup(t *testing.T) {
// Kode setup berjalan sekali, sebelum eksekusi paralel
db := setupTestDatabase(t)
defer db.Close()
tests := []struct {
name string
id int
}{
{"user1", 1},
{"user2", 2},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Setiap pengujian menggunakan db secara independen
user := db.GetUser(tt.id)
// Logika pengujian...
})
}
}
Pola 2: Setup Per Pengujian
Untuk pengujian yang membutuhkan setup terisolasi, lakukan di dalam setiap subtest:
func TestWithPerTestSetup(t *testing.T) {
tests := []struct {
name string
data string
}{
{"test1", "data1"},
{"test2", "data2"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Setiap pengujian mendapatkan setup sendiri
tempFile := createTempFile(t, tt.data)
defer os.Remove(tempFile)
// Logika pengujian...
})
}
}
Pola 3: Campuran Berurutan dan Paralel
Anda dapat mencampur pengujian berurutan dan paralel dalam file yang sama:
func TestSequential(t *testing.T) {
// Tidak ada t.Parallel() - berjalan berurutan
// Baik untuk pengujian yang harus berjalan dalam urutan tertentu
}
func TestParallel(t *testing.T) {
tests := []struct{ name string }{{"test1"}, {"test2"}}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Ini berjalan secara paralel
})
}
}
Kapan Tidak Harus Menggunakan Eksekusi Paralel
Eksekusi paralel tidak selalu sesuai. Hindari penggunaannya ketika:
- Pengujian berbagi state: Variabel global, singleton, atau sumber daya bersama
- Pengujian memodifikasi file bersama: File sementara, database pengujian, atau file konfigurasi
- Pengujian bergantung pada urutan eksekusi: Beberapa pengujian harus berjalan sebelum yang lain
- Pengujian sudah cepat: Biaya paralelisasi mungkin melebihi manfaatnya
- Keterbatasan sumber daya: Pengujian mengonsumsi terlalu banyak memori atau CPU ketika diparalelisasi
Untuk pengujian terkait database, pertimbangkan penggunaan rollback transaksi atau database pengujian terpisah per pengujian. Panduan kami tentang pola database multi-tenant di Go mencakup strategi isolasi yang berfungsi baik dengan pengujian paralel.
Lanjutan: Pengujian Kode Konkuren
Ketika menguji kode konkuren itu sendiri (bukan hanya menjalankan pengujian secara paralel), Anda membutuhkan teknik tambahan:
func TestConcurrentOperation(t *testing.T) {
tests := []struct {
name string
goroutines int
}{
{"2 goroutines", 2},
{"10 goroutines", 10},
{"100 goroutines", 100},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var wg sync.WaitGroup
results := make(chan int, tt.goroutines)
for i := 0; i < tt.goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
results <- performOperation()
}()
}
wg.Wait()
close(results)
// Verifikasi hasil
count := 0
for range results {
count++
}
if count != tt.goroutines {
t.Errorf("expected %d results, got %d", tt.goroutines, count)
}
})
}
}
Selalu jalankan pengujian seperti ini dengan -race untuk mendeteksi data race dalam kode yang diuji.
Integrasi dengan CI/CD
Pengujian paralel terintegrasi dengan mulus dengan pipeline CI/CD. Sebagian besar sistem CI menyediakan beberapa inti CPU, membuat eksekusi paralel sangat bermanfaat:
# Contoh GitHub Actions
- name: Run tests
run: |
go test -race -coverprofile=coverage.out -parallel 4 ./...
go tool cover -html=coverage.out -o coverage.html
Flag -race sangat penting dalam CI untuk menangkap bug konkurensi yang mungkin tidak muncul dalam pengembangan lokal.
Debugging Gagalnya Pengujian Paralel
Ketika pengujian paralel gagal secara tidak terduga, debugging bisa menjadi tantangan:
- Jalankan dengan
-race: Identifikasi data race - Kurangi paralelisme:
go test -parallel 1untuk melihat apakah kegagalan hilang - Jalankan pengujian spesifik:
go test -run TestNameuntuk mengisolasi masalah - Tambahkan logging: Gunakan
t.Log()untuk melacak urutan eksekusi - Periksa state bersama: Cari variabel global, singleton, atau sumber daya bersama
Jika pengujian berhasil berurutan tetapi gagal secara paralel, kemungkinan besar Anda memiliki masalah kondisi race atau state bersama.
Contoh Nyata: Pengujian Handler HTTP
Berikut adalah contoh praktis pengujian handler HTTP secara paralel:
func TestHTTPHandlers(t *testing.T) {
router := setupRouter()
tests := []struct {
name string
method string
path string
statusCode int
}{
{"GET users", "GET", "/users", 200},
{"GET user by ID", "GET", "/users/1", 200},
{"POST user", "POST", "/users", 201},
{"DELETE user", "DELETE", "/users/1", 204},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(tt.method, tt.path, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tt.statusCode {
t.Errorf("expected status %d, got %d", tt.statusCode, w.Code)
}
})
}
}
Setiap pengujian menggunakan httptest.NewRecorder(), yang menciptakan recorder respons terisolasi, membuat pengujian ini aman untuk eksekusi paralel.
Kesimpulan
Eksekusi paralel dari pengujian berbasis tabel adalah teknik yang kuat untuk mengurangi waktu eksekusi suite pengujian di Go. Kunci keberhasilan adalah memahami kebutuhan penangkapan variabel loop, memastikan kemandirian pengujian, dan menggunakan detektor race untuk menangkap masalah konkurensi secara dini.
Ingatlah:
- Selalu tangkap variabel loop:
tt := ttsebelumt.Parallel() - Pastikan pengujian mandiri tanpa state bersama
- Jalankan pengujian dengan
-raceselama pengembangan - Kontrol paralelisme dengan flag
-parallelketika diperlukan - Hindari eksekusi paralel untuk pengujian yang berbagi sumber daya
Dengan mengikuti praktik-praktik ini, Anda dapat aman memanfaatkan eksekusi paralel untuk mempercepat suite pengujian Anda sambil mempertahankan keandalan. Untuk lebih banyak pola pengujian Go, lihat panduan komprehensif kami tentang Go unit testing, yang mencakup pengujian berbasis tabel, mocking, dan teknik pengujian penting lainnya.
Ketika membangun aplikasi Go yang lebih besar, praktik pengujian ini berlaku di berbagai domain. Misalnya, ketika membangun aplikasi CLI dengan Cobra & Viper, Anda akan menggunakan pola pengujian paralel yang serupa untuk menguji handler perintah dan parsing konfigurasi.
Tautan Berguna
- Go Cheatsheet
- Go Unit Testing: Struktur & Best Practices
- Membangun Aplikasi CLI dalam Go dengan Cobra & Viper
- Pola Database Multi-Tenant dengan contoh dalam Go
- Go ORMs untuk PostgreSQL: GORM vs Ent vs Bun vs sqlc