Uji Unit dengan Go: Struktur & Praktik Terbaik
Pengujian Go dari dasar hingga pola lanjutan
Package pengujian bawaan Go menyediakan kerangka kerja yang kuat dan minimalis untuk menulis pengujian unit tanpa ketergantungan eksternal. Berikut adalah dasar-dasar pengujian, struktur proyek, dan pola lanjutan untuk membangun aplikasi Go yang andal.

Mengapa Pengujian Penting dalam Go
Filosofi Go menekankan kesederhanaan dan keandalan. Perpustakaan standar mencakup paket testing, membuat pengujian unit menjadi warga negara kelas satu dalam ekosistem Go. Kode Go yang telah diuji dengan baik meningkatkan keterpeliharaan, menangkap bug sejak dini, dan menyediakan dokumentasi melalui contoh. Jika Anda baru dengan Go, lihat Kartu Panduan Go kami untuk referensi cepat tentang dasar-dasar bahasa tersebut.
Manfaat utama pengujian Go:
- Dukungan bawa-in: Tidak diperlukan kerangka kerja eksternal
- Eksekusi cepat: Eksekusi pengujian secara paralel secara default
- Sintaks sederhana: Kode boilerplate minimal
- Alat yang kaya: Laporan cakupan, benchmark, dan profiling
- Ramah CI/CD: Integrasi mudah dengan pipeline otomatis
Struktur Proyek untuk Pengujian Go
Pengujian Go berada di samping kode produksi Anda dengan konvensi penamaan yang jelas:
myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│ ├── helper.go
│ └── helper_test.go
└── models/
├── user.go
└── user_test.go
Konvensi penting:
- File pengujian diakhiri dengan
_test.go - Pengujian berada dalam paket yang sama dengan kode (atau menggunakan sufiks
_testuntuk pengujian kotak hitam) - Setiap file sumber dapat memiliki file pengujian yang sesuai
Pendekatan Pengujian Paket
Pengujian kotak putih (sama paket):
package calculator
import "testing"
// Bisa mengakses fungsi dan variabel yang tidak diekspor
Pengujian kotak hitam (paket eksternal):
package calculator_test
import (
"testing"
"myproject/calculator"
)
// Hanya bisa mengakses fungsi yang diekspor (direkomendasikan untuk API publik)
Struktur Dasar Pengujian
Setiap fungsi pengujian mengikuti pola ini:
package calculator
import "testing"
// Fungsi pengujian harus dimulai dengan "Test"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Metode testing.T:
t.Error()/t.Errorf(): Tandai pengujian sebagai gagal tetapi lanjutkant.Fatal()/t.Fatalf(): Tandai pengujian sebagai gagal dan berhenti segerat.Log()/t.Logf(): Log output (hanya ditampilkan dengan flag-v)t.Skip()/t.Skipf(): Lewati pengujiant.Parallel(): Jalankan pengujian secara paralel dengan pengujian paralel lainnya
Pengujian Berbasis Tabel: Cara Go
Pengujian berbasis tabel adalah pendekatan idiomatic Go untuk menguji berbagai skenario. Dengan Generik Go, Anda juga dapat membuat bantuan pengujian yang aman secara tipe yang bekerja di berbagai jenis data:
func TestCalculate(t *testing.T) {
tests := []struct {
name string
a, b int
op string
expected int
wantErr bool
}{
{"penjumlahan", 2, 3, "+", 5, false},
{"pengurangan", 5, 3, "-", 2, false},
{"perkalian", 4, 3, "*", 12, false},
{"pembagian", 10, 2, "/", 5, false},
{"pembagian dengan nol", 10, 0, "/", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
}
})
}
}
Keuntungan:
- Satu fungsi pengujian untuk berbagai skenario
- Mudah menambahkan kasus pengujian baru
- Dokumentasi jelas tentang perilaku yang diharapkan
- Organisasi dan pemeliharaan pengujian yang lebih baik
Menjalankan Pengujian
Perintah Dasar
# Jalankan pengujian di direktori saat ini
go test
# Jalankan pengujian dengan output rinci
go test -v
# Jalankan pengujian di semua subdirektori
go test ./...
# Jalankan pengujian spesifik
go test -run TestAdd
# Jalankan pengujian dengan pola
go test -run TestCalculate/addition
# Jalankan pengujian secara paralel (default adalah GOMAXPROCS)
go test -parallel 4
# Jalankan pengujian dengan timeout
go test -timeout 30s
Cakupan Pengujian
# Jalankan pengujian dengan cakupan
go test -cover
# Buat profil cakupan
go test -coverprofile=coverage.out
# Tampilkan cakupan di browser
go tool cover -html=coverage.out
# Tampilkan cakupan berdasarkan fungsi
go tool cover -func=coverage.out
# Tetapkan mode cakupan (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out
Flag yang Berguna
-short: Jalankan pengujian yang ditandai denganif testing.Short()-race: Aktifkan detektor race (mencari masalah akses konkuren)-cpu: Tentukan nilai GOMAXPROCS-count n: Jalankan setiap pengujian n kali-failfast: Berhenti pada kegagalan pengujian pertama
Bantuan Pengujian dan Setup/Teardown
Fungsi Bantuan
Tandai fungsi bantuan dengan t.Helper() untuk meningkatkan pelaporan kesalahan:
func assertEqual(t *testing.T, got, want int) {
t.Helper() // Baris ini dilaporkan sebagai pemanggil
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestMath(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5) // Baris kesalahan menunjuk ke sini
}
Setup dan Teardown
func TestMain(m *testing.M) {
// Kode setup di sini
setup()
// Jalankan pengujian
code := m.Run()
// Kode teardown di sini
teardown()
os.Exit(code)
}
Fixtures Pengujian
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("setup test case")
return func(t *testing.T) {
t.Log("teardown test case")
}
}
func TestSomething(t *testing.T) {
teardown := setupTestCase(t)
defer teardown(t)
// Kode pengujian di sini
}
Pemalsuan dan Injeksi Ketergantungan
Pemalsuan Berbasis Interface
Ketika menguji kode yang berinteraksi dengan database, menggunakan interface membuatnya mudah untuk membuat implementasi pemalsuan. Jika Anda bekerja dengan PostgreSQL dalam Go, lihat perbandingan ORMs Go kami untuk memilih perpustakaan database yang tepat dengan tingkat pengujian yang baik.
// Kode produksi
type Database interface {
GetUser(id int) (*User, error)
}
type UserService struct {
db Database
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.db.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
// Kode pengujian
type MockDatabase struct {
users map[int]*User
}
func (m *MockDatabase) GetUser(id int) (*User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, errors.New("user not found")
}
func TestGetUserName(t *testing.T) {
mockDB := &MockDatabase{
users: map[int]*User{
1: {ID: 1, Name: "Alice"},
},
}
service := &UserService{db: mockDB}
name, err := service.GetUserName(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if name != "Alice" {
t.Errorf("got %s, want Alice", name)
}
}
Perpustakaan Pengujian Populer
Testify
Perpustakaan pengujian Go paling populer untuk asersi dan pemalsuan:
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestWithTestify(t *testing.T) {
result := Add(2, 3)
assert.Equal(t, 5, result, "they should be equal")
assert.NotNil(t, result)
}
// Contoh pemalsuan
type MockDB struct {
mock.Mock
}
func (m *MockDB) GetUser(id int) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
Alat Lain
- gomock: Kerangka kerja pemalsuan Google dengan pembangkitan kode
- httptest: Perpustakaan standar untuk menguji penangan HTTP
- testcontainers-go: Pengujian integrasi dengan kontainer Docker
- ginkgo/gomega: Kerangka kerja pengujian gaya BDD
Ketika menguji integrasi dengan layanan eksternal seperti model AI, Anda perlu memalsukan atau menstub ketergantungan tersebut. Misalnya, jika Anda menggunakan Ollama dalam Go, pertimbangkan membuat pengepakan interface untuk membuat kode Anda lebih mudah diuji.
Pengujian Benchmark
Go menyediakan dukungan bawa-in untuk benchmark:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Jalankan benchmark
// go test -bench=. -benchmem
Output menunjukkan iterasi per detik dan alokasi memori.
Praktik Terbaik
- Tulis pengujian berbasis tabel: Gunakan pola slice of structs untuk berbagai kasus pengujian
- Gunakan
t.Rununtuk subtest: Organisasi yang lebih baik dan bisa menjalankan subtest secara selektif - Uji fungsi yang diekspor terlebih dahulu: Fokus pada perilaku API publik
- Jaga pengujian sederhana: Setiap pengujian harus memverifikasi satu hal
- Gunakan nama pengujian yang bermakna: Deskripsikan apa yang diuji dan hasil yang diharapkan
- Jangan uji detail implementasi: Uji perilaku, bukan internal
- Gunakan interface untuk ketergantungan: Membuat pemalsuan lebih mudah
- Arahkan pada cakupan tinggi, tetapi kualitas lebih dari kuantitas: Cakupan 100% tidak berarti bebas bug
- Jalankan pengujian dengan flag -race: Tangkap masalah konkuren sejak dini
- Gunakan
TestMainuntuk setup mahal: Hindari mengulang setup di setiap pengujian
Contoh: Suite Pengujian Lengkap
package user
import (
"errors"
"testing"
)
type User struct {
ID int
Name string
Email string
}
func ValidateUser(u *User) error {
if u.Name == "" {
return errors.New("name cannot be empty")
}
if u.Email == "" {
return errors.New("email cannot be empty")
}
return nil
}
// File pengujian: user_test.go
func TestValidateUser(t *testing.T) {
tests := []struct {
name string
user *User
wantErr bool
errMsg string
}{
{
name: "user valid",
user: &User{ID: 1, Name: "Alice", Email: "alice@example.com"},
wantErr: false,
},
{
name: "nama kosong",
user: &User{ID: 1, Name: "", Email: "alice@example.com"},
wantErr: true,
errMsg: "name cannot be empty",
},
{
name: "email kosong",
user: &User{ID: 1, Name: "Alice", Email: ""},
wantErr: true,
errMsg: "email cannot be empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.user)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && err.Error() != tt.errMsg {
t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
}
})
}
}
Tautan Berguna
- Dokumentasi Resmi Paket Pengujian Go
- Blog Go: Pengujian Berbasis Tabel
- Repository GitHub Testify
- Dokumentasi GoMock
- Belajar Go dengan Pengujian
- Alat Cakupan Kode Go
- Kartu Panduan Go
- Perbandingan ORMs Go untuk PostgreSQL: GORM vs Ent vs Bun vs sqlc
- SDK Go untuk Ollama - perbandingan dengan contoh
- Membangun Aplikasi CLI dalam Go dengan Cobra & Viper
- Generik Go: Kasus Penggunaan dan Pola
Kesimpulan
Kerangka kerja pengujian Go menyediakan segala yang diperlukan untuk pengujian unit menyeluruh dengan pengaturan minimal. Dengan mengikuti idiom Go seperti pengujian berbasis tabel, menggunakan interface untuk pemalsuan, dan memanfaatkan alat bawa-in, Anda dapat menciptakan suite pengujian yang dapat dipelihara dan andal yang tumbuh bersama dengan kode Anda.
Praktik pengujian ini berlaku untuk semua jenis aplikasi Go, dari layanan web hingga aplikasi CLI yang dibangun dengan Cobra & Viper. Pengujian alat baris perintah memerlukan pola serupa dengan fokus tambahan pada pengujian input/output dan parsing flag.
Mulailah dengan pengujian sederhana, secara bertahap tambahkan cakupan, dan ingat bahwa pengujian adalah investasi dalam kualitas kode dan kepercayaan pengembang. Penekanan komunitas Go pada pengujian membuatnya lebih mudah untuk mempertahankan proyek jangka panjang dan bekerja sama secara efektif dengan rekan tim.