Test pada chapter sebelumnya menguji fungsi yang berdiri sendiri; kode dunia nyata hampir selalu bergantung pada layanan eksternal: email, basis data, atau API pihak ketiga. Menjalankan dependency sungguhan di dalam test membuat test lambat, rapuh, dan berisiko mengirim email asli ke pelanggan sungguhan. Solusinya adalah soal desain, bukan soal alat: kita rancang dependency sebagai interface yang kecil, suntikkan lewat constructor, lalu ganti dengan pengganti tiruan (test double) saat test berjalan. Pada chapter terakhir section ini kita akan membangun layanan pendaftaran yang bergantung pada Notifier, dan menguji semuanya tanpa menyentuh jaringan.
Masalah Ketergantungan Langsung #
Bayangkan PendaftaranService memegang implementasi email secara langsung:
type PendaftaranService struct {
notifier *EmailNotifier // kopling langsung ke implementasi konkret
}Struktur seperti ini menyulitkan pengujian dari dua sisi: test tidak bisa memakai service tanpa membawa EmailNotifier beserta jaringannya, dan isi pesan yang “terkirim” tidak bisa diamati. Service juga kaku — mengganti email dengan SMS berarti mengubah service, bukan menukar komponen.
Interface Kecil sebagai Kontrak #
Kita balik rancangannya: service hanya mengenal kontrak Notifier berisi satu method. Simpan kode berikut sebagai notifikasi.go:
package main
import (
"fmt"
"strings"
"sync"
)
// Pesan merekam satu notifikasi yang terkirim.
type Pesan struct {
Ke string
Subjek string
Isi string
}
// Notifier adalah abstraksi cara mengirim pesan ke pengguna.
// Implementasi nyata bisa memakai SMTP, webhook, atau antrean pesan.
type Notifier interface {
Kirim(ke, subjek, isi string) error
}
// EmailNotifier adalah implementasi untuk produksi. Pada contoh ini
// pengiriman disimulasikan ke log memori agar program dapat dijalankan
// tanpa jaringan; versi sungguhan memanggil server SMTP.
type EmailNotifier struct {
mu sync.Mutex
terkirim []Pesan
}
// Kirim mencatat pesan ke log memori seolah terkirim lewat email.
func (e *EmailNotifier) Kirim(ke, subjek, isi string) error {
if ke == "" {
return fmt.Errorf("alamat tujuan kosong")
}
e.mu.Lock()
defer e.mu.Unlock()
e.terkirim = append(e.terkirim, Pesan{Ke: ke, Subjek: subjek, Isi: isi})
return nil
}
// Terkirim mengembalikan salinan daftar pesan yang sudah tercatat.
func (e *EmailNotifier) Terkirim() []Pesan {
e.mu.Lock()
defer e.mu.Unlock()
return append([]Pesan(nil), e.terkirim...)
}
// PendaftaranService melayani pendaftaran pengguna baru lalu mengirim
// ucapan selamat datang lewat Notifier.
type PendaftaranService struct {
notifier Notifier
}
// NewPendaftaranService menyuntikkan dependency lewat constructor,
// sehingga pemanggil yang menentukan implementasi Notifier mana yang dipakai.
func NewPendaftaranService(n Notifier) *PendaftaranService {
return &PendaftaranService{notifier: n}
}
// Daftar mendaftarkan email yang valid, lalu memberi tahu pengguna.
func (s *PendaftaranService) Daftar(email string) error {
if !strings.Contains(email, "@") || len(email) < 5 {
return fmt.Errorf("email tidak valid: %q", email)
}
return s.notifier.Kirim(email, "Selamat datang", "Terima kasih sudah mendaftar di toko kami.")
}
func main() {
email := &EmailNotifier{}
layanan := NewPendaftaranService(email)
_ = layanan.Daftar("[email protected]")
_ = layanan.Daftar("[email protected]")
for _, p := range email.Terkirim() {
fmt.Printf("ke=%s subjek=%q\n", p.Ke, p.Subjek)
}
}Jalankan berkas tersebut dari folder chapter:
go run notifikasi.goOutput yang Dihasilkan:
[email protected] subjek="Selamat datang"
[email protected] subjek="Selamat datang"Mari kita telaah bagian-bagian utamanya:
- Baris 18-20 (
type Notifier interface): kontrak satu method — sekecil mungkin. Siapa pun yang punya methodKirimdengan tanda tangan sama otomatis memenuhi interface ini, tanpa deklarasi eksplisit. - Baris 25-46 (
EmailNotifier): implementasi produksi. Versi chapter ini mencatat pesan ke slice berproteksi mutex agar contoh dapat berjalan offline; di proyek nyata isiKirimdiganti pemanggilan SMTP. - Baris 50-58 (
PendaftaranServicedan constructor-nya): service menyimpan field bertipeNotifier, danNewPendaftaranServicemenerima implementasi dari luar — pola dependency injection lewat constructor. - Baris 61-66 (
func Daftar): alur service — validasi email lalus.notifier.Kirim. Service tidak tahu dan tidak perlu tahu media pengiriman yang dipakai. - Baris 68-76 (
func main): menyambungkanEmailNotifierke service lalu mencetak pesan yang tercatat, sebagai bukti alur utuh berjalan.
Test dengan Pengganti Tiruan #
Berkas notifikasi_test.go mendefinisikan FakeNotifier — pengganti yang merekam setiap panggilan — lalu mengujinya lewat tiga skenario:
package main
import (
"fmt"
"testing"
)
// FakeNotifier merekam setiap panggilan Kirim sehingga test dapat
// memeriksa perilaku PendaftaranService tanpa jaringan sama sekali.
type FakeNotifier struct {
panggilan []Pesan
gagal bool
}
// Kirim mencatat panggilan, atau mengembalikan galat bila mode gagal aktif.
func (f *FakeNotifier) Kirim(ke, subjek, isi string) error {
if f.gagal {
return fmt.Errorf("jaringan bermasalah")
}
f.panggilan = append(f.panggilan, Pesan{Ke: ke, Subjek: subjek, Isi: isi})
return nil
}
func TestDaftarBerhasilMengirimNotifikasi(t *testing.T) {
fake := &FakeNotifier{}
layanan := NewPendaftaranService(fake)
if err := layanan.Daftar("[email protected]"); err != nil {
t.Fatalf("Daftar gagal: %v", err)
}
if len(fake.panggilan) != 1 {
t.Fatalf("jumlah panggilan Kirim = %d, diharapkan 1", len(fake.panggilan))
}
p := fake.panggilan[0]
if p.Ke != "[email protected]" || p.Subjek != "Selamat datang" {
t.Errorf("isi pesan tidak sesuai: %+v", p)
}
}
func TestDaftarEmailTidakValid(t *testing.T) {
fake := &FakeNotifier{}
layanan := NewPendaftaranService(fake)
if err := layanan.Daftar("budi-tanpa-at"); err == nil {
t.Fatal("Daftar seharusnya gagal untuk email tanpa karakter @")
}
if len(fake.panggilan) != 0 {
t.Errorf("tidak boleh ada notifikasi terkirim, tercatat %d", len(fake.panggilan))
}
}
func TestDaftarGagalKirimMeneruskanGalat(t *testing.T) {
fake := &FakeNotifier{gagal: true}
layanan := NewPendaftaranService(fake)
if err := layanan.Daftar("[email protected]"); err == nil {
t.Fatal("galat dari Notifier seharusnya diteruskan ke pemanggil")
}
}Jalankan dari folder chapter:
go test -v ./...Output yang Dihasilkan:
=== RUN TestDaftarBerhasilMengirimNotifikasi
--- PASS: TestDaftarBerhasilMengirimNotifikasi (0.00s)
=== RUN TestDaftarEmailTidakValid
--- PASS: TestDaftarEmailTidakValid (0.00s)
=== RUN TestDaftarGagalKirimMeneruskanGalat
--- PASS: TestDaftarGagalKirimMeneruskanGalat (0.00s)
PASS
ok tutorial-website/content/go/testing/testing-package-with-dependencies 0.001sTiga skenario pada berkas tersebut sengaja membeda-bedakan jalur perilaku:
- Baris 24-38 (
TestDaftarBerhasilMengirimNotifikasi): jalur bahagia. Test memeriksa jumlah panggilanKirimtepat satu dan isi parameternya sesuai — pemeriksaan interaksi yang hanya mungkin bila dependency bisa diamati. - Baris 40-50 (
TestDaftarEmailTidakValid): email tanpa@harus ditolak sebelum notifikasi terkirim; assertion kedua memastikan nol panggilan. - Baris 52-59 (
TestDaftarGagalKirimMeneruskanGalat):FakeNotifierdiatur modegagal: truesehinggaKirimmengembalikan galat — test memastikan service meneruskan galat itu ke pemanggil, bukan menelannya.
Stub, Fake, dan Mock #
Istilah pengganti uji sering dipakai campur aduk; tabel berikut memisahkannya menurut definisi yang lazim:
| Istilah | Definisi | Contoh pada chapter ini |
|---|---|---|
| Stub | Pengganti yang selalu menjawab nilai tetap; interaksi tidak diperiksa | struct dengan method yang langsung return nil |
| Fake | Implementasi ringan yang benar-benar berfungsi tanpa infrastruktur berat | EmailNotifier berbasis log memori menggantikan SMTP |
| Mock | Pengganti yang merekam interaksi agar test bisa memverifikasi panggilan | FakeNotifier yang menyimpan setiap panggilan Kirim |
FakeNotifier pada prinsipnya berperan sebagai mock, sementara EmailNotifier versi memori adalah fake dari SMTP sungguhan. Komunitas Go cenderung longgar menyebut semua pengganti uji sebagai fake — nama pada contoh mengikuti kebiasaan itu; yang penting test Anda memeriksa perilaku yang benar, bukan debat label.
Kenapa Interface Harus Kecil #
Interface satu method memberi tiga keuntungan sekaligus. Pengganti uji cukup mengimplementasikan satu method sehingga biaya pembuatannya nyaris nol; service berganti implementasi (SMTP, SMS, antrean) tanpa kompilasi ulang logika; dan kontrak tetap tajam karena tidak memuat method yang tidak dipakai. Prinsip ini sejalan dengan pembahasan Dependency Inversion : bergantung pada abstraksi, bukan implementasi konkret.
Pola yang sama berskala besar pada proyek nyata. Chapter REST API dengan PostgreSQL
menyuntikkan repository basis data sebagai interface ke service, dan package httptest yang dibahas pada chapter httptest
menyediakan server HTTP tiruan untuk kebutuhan yang sama. Setiap dependency eksternal yang terkungkung interface kecil membuka pintu bagi test yang cepat dan deterministik.
Referensi #
- Package testing
— dasar dari seluruh berkas
_test.gopada chapter ini. - Tutorial: Add a test — tutorial resmi penulisan test pertama.
- Effective Go — filosofi interface di bagian interfaces.
Dengan dependency yang terkungkung interface kecil, test Anda menjangkau logika bisnis tanpa biaya jaringan atau basis data — dan seluruh perangkat section Testing kini lengkap: dari unit test, benchmark, fuzzing, contoh terverifikasi, coverage, race detector, hingga desain yang bisa diuji. Ketika kode yang sudah teruji tetap gagal di lapangan, saatnya berpindah ke section Debugging dan Profiling untuk membedah penyebabnya.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
notifikasi_test.go
Unduh berkaspackage main
import (
"fmt"
"testing"
)
// FakeNotifier merekam setiap panggilan Kirim sehingga test dapat
// memeriksa perilaku PendaftaranService tanpa jaringan sama sekali.
type FakeNotifier struct {
panggilan []Pesan
gagal bool
}
// Kirim mencatat panggilan, atau mengembalikan galat bila mode gagal aktif.
func (f *FakeNotifier) Kirim(ke, subjek, isi string) error {
if f.gagal {
return fmt.Errorf("jaringan bermasalah")
}
f.panggilan = append(f.panggilan, Pesan{Ke: ke, Subjek: subjek, Isi: isi})
return nil
}
func TestDaftarBerhasilMengirimNotifikasi(t *testing.T) {
fake := &FakeNotifier{}
layanan := NewPendaftaranService(fake)
if err := layanan.Daftar("[email protected]"); err != nil {
t.Fatalf("Daftar gagal: %v", err)
}
if len(fake.panggilan) != 1 {
t.Fatalf("jumlah panggilan Kirim = %d, diharapkan 1", len(fake.panggilan))
}
p := fake.panggilan[0]
if p.Ke != "[email protected]" || p.Subjek != "Selamat datang" {
t.Errorf("isi pesan tidak sesuai: %+v", p)
}
}
func TestDaftarEmailTidakValid(t *testing.T) {
fake := &FakeNotifier{}
layanan := NewPendaftaranService(fake)
if err := layanan.Daftar("budi-tanpa-at"); err == nil {
t.Fatal("Daftar seharusnya gagal untuk email tanpa karakter @")
}
if len(fake.panggilan) != 0 {
t.Errorf("tidak boleh ada notifikasi terkirim, tercatat %d", len(fake.panggilan))
}
}
func TestDaftarGagalKirimMeneruskanGalat(t *testing.T) {
fake := &FakeNotifier{gagal: true}
layanan := NewPendaftaranService(fake)
if err := layanan.Daftar("[email protected]"); err == nil {
t.Fatal("galat dari Notifier seharusnya diteruskan ke pemanggil")
}
}
notifikasi.go
Unduh berkaspackage main
import (
"fmt"
"strings"
"sync"
)
// Pesan merekam satu notifikasi yang terkirim.
type Pesan struct {
Ke string
Subjek string
Isi string
}
// Notifier adalah abstraksi cara mengirim pesan ke pengguna.
// Implementasi nyata bisa memakai SMTP, webhook, atau antrean pesan.
type Notifier interface {
Kirim(ke, subjek, isi string) error
}
// EmailNotifier adalah implementasi untuk produksi. Pada contoh ini
// pengiriman disimulasikan ke log memori agar program dapat dijalankan
// tanpa jaringan; versi sungguhan memanggil server SMTP.
type EmailNotifier struct {
mu sync.Mutex
terkirim []Pesan
}
// Kirim mencatat pesan ke log memori seolah terkirim lewat email.
func (e *EmailNotifier) Kirim(ke, subjek, isi string) error {
if ke == "" {
return fmt.Errorf("alamat tujuan kosong")
}
e.mu.Lock()
defer e.mu.Unlock()
e.terkirim = append(e.terkirim, Pesan{Ke: ke, Subjek: subjek, Isi: isi})
return nil
}
// Terkirim mengembalikan salinan daftar pesan yang sudah tercatat.
func (e *EmailNotifier) Terkirim() []Pesan {
e.mu.Lock()
defer e.mu.Unlock()
return append([]Pesan(nil), e.terkirim...)
}
// PendaftaranService melayani pendaftaran pengguna baru lalu mengirim
// ucapan selamat datang lewat Notifier.
type PendaftaranService struct {
notifier Notifier
}
// NewPendaftaranService menyuntikkan dependency lewat constructor,
// sehingga pemanggil yang menentukan implementasi Notifier mana yang dipakai.
func NewPendaftaranService(n Notifier) *PendaftaranService {
return &PendaftaranService{notifier: n}
}
// Daftar mendaftarkan email yang valid, lalu memberi tahu pengguna.
func (s *PendaftaranService) Daftar(email string) error {
if !strings.Contains(email, "@") || len(email) < 5 {
return fmt.Errorf("email tidak valid: %q", email)
}
return s.notifier.Kirim(email, "Selamat datang", "Terima kasih sudah mendaftar di toko kami.")
}
func main() {
email := &EmailNotifier{}
layanan := NewPendaftaranService(email)
_ = layanan.Daftar("[email protected]")
_ = layanan.Daftar("[email protected]")
for _, p := range email.Terkirim() {
fmt.Printf("ke=%s subjek=%q\n", p.Ke, p.Subjek)
}
}