Strategi uji GUI Go lintas platform yang efektif tidak dimulai dari otomatisasi klik tombol, melainkan dari pemisahan tanggung jawab yang jelas: logika state harus dapat diuji tanpa window, kode integrasi native harus diuji lewat kontrak, dan pengujian UI end-to-end dipakai terbatas untuk alur kritis. Pendekatan ini mengurangi regresi yang hanya muncul di Windows, macOS, atau Linux tanpa membuat pipeline CI lambat dan rapuh.
Shirei dapat dipakai sebagai konteks referensi proyek GUI Go native, bukan sebagai objek ulasan. Saat menerapkan strategi ini pada proyek seperti Shirei, prinsip utamanya adalah menjaga kode yang bergantung pada event loop, window, renderer, clipboard, dialog, atau API OS tetap tipis. Sebagian besar aturan bisnis dan transisi antarmuka sebaiknya berada pada kode Go murni yang deterministik.
Mengapa regresi GUI native sulit ditangkap
GUI native bukan sekadar fungsi yang menerima input dan segera mengembalikan output. Aplikasi berinteraksi dengan scheduler OS, event loop toolkit, compositor, driver grafis, layanan desktop, dan perangkat input. Akibatnya, tes yang lulus di mesin pengembang belum tentu mewakili perilaku runner CI atau OS lain.
- Perbedaan OS: urutan event fokus, pintasan keyboard, format path, izin file, DPI/scaling, font, dan perilaku dialog sistem dapat berbeda.
- Event asynchronous: callback klik, pembacaan file, timer, goroutine, dan pembaruan UI dapat selesai dalam urutan yang tidak tetap.
- Timing render: perintah mengubah state tidak selalu langsung terlihat pada frame yang sama. Menunggu durasi tetap sering menghasilkan tes flaky.
- Input pengguna: double click, input method editor, drag-and-drop, fokus window, serta kombinasi tombol modifier memiliki variasi platform dan perangkat.
- Integrasi native: clipboard, file picker, notifikasi, tray, penyimpanan kredensial, atau API window manager dapat gagal karena dependency OS, sandbox, session desktop, atau lingkungan headless.
Jangan menjadikan screenshot atau automation klik sebagai satu-satunya bukti kualitas GUI. Tes visual dan E2E bagus untuk memvalidasi integrasi, tetapi bukan pengganti tes logika state yang cepat dan deterministik.
Rancang test pyramid: state, adapter platform, lalu UI
Piramida berikut meminimalkan biaya eksekusi dan memusatkan kegagalan pada lapisan yang benar. Proporsi tepatnya bergantung pada aplikasi, tetapi jumlah tes cenderung menurun dari bawah ke atas.
1. Dasar piramida: unit test untuk state dan reducer murni
Modelkan interaksi pengguna sebagai action, lalu ubah state melalui reducer atau command handler murni. Reducer tidak membuka window, tidak memanggil clipboard, tidak memakai time.Sleep, dan tidak menjalankan goroutine. Karena input serta output-nya eksplisit, tes dapat berjalan cepat di semua OS, termasuk pada CI tanpa sesi desktop.
package editor
import "strings"
type State struct {
Text string
Dirty bool
Status string
}
type Action interface{ isAction() }
type TextChanged struct{ Value string }
func (TextChanged) isAction() {}
type SaveSucceeded struct{}
func (SaveSucceeded) isAction() {}
func Reduce(s State, action Action) State {
switch a := action.(type) {
case TextChanged:
s.Text = a.Value
s.Dirty = true
s.Status = "Belum disimpan"
case SaveSucceeded:
s.Dirty = false
s.Status = "Tersimpan"
}
return s
}
func CanSave(s State) bool {
return s.Dirty && strings.TrimSpace(s.Text) != ""
}package editor
import "testing"
func TestTextChangedMarksDocumentDirty(t *testing.T) {
initial := State{Text: "lama", Status: "Tersimpan"}
got := Reduce(initial, TextChanged{Value: "baru"})
if got.Text != "baru" || !got.Dirty || got.Status != "Belum disimpan" {
t.Fatalf("state tidak sesuai: %+v", got)
}
}
func TestCanSaveRejectsWhitespaceOnlyText(t *testing.T) {
state := State{Text: " ", Dirty: true}
if CanSave(state) {
t.Fatal("dokumen kosong tidak boleh dapat disimpan")
}
}Handler yang membutuhkan efek samping dapat mengembalikan effect sebagai data, misalnya permintaan menyimpan file atau menyalin teks. Lapisan UI kemudian mengeksekusi effect tersebut melalui adapter. Dengan pola ini, urutan perubahan state dapat diuji tanpa ketergantungan native.
2. Tengah piramida: contract test untuk adapter platform
Adapter menyembunyikan detail API native di balik interface kecil. Contohnya adalah clipboard, pemilih berkas, atau penyimpanan preferensi. Hindari interface besar yang meniru seluruh toolkit; definisikan hanya kemampuan yang dibutuhkan domain.
package platform
import "context"
type Clipboard interface {
ReadText(ctx context.Context) (string, error)
WriteText(ctx context.Context, text string) error
}Contract test mendefinisikan perilaku yang wajib dipenuhi semua implementasi adapter. Tes kontrak yang sama dijalankan terhadap fake in-memory dan implementasi native yang tersedia pada OS runner. Misalnya, setelah WriteText berhasil, ReadText harus mengembalikan teks yang sama. Jika clipboard native tidak tersedia pada lingkungan tertentu, tes harus skip dengan alasan eksplisit, bukan diam-diam dianggap lulus.
package platform_test
import (
"context"
"testing"
"example.com/app/platform"
)
func ClipboardContract(t *testing.T, newClipboard func(t *testing.T) platform.Clipboard) {
t.Helper()
cb := newClipboard(t)
ctx := context.Background()
const want = "salin lintas platform"
if err := cb.WriteText(ctx, want); err != nil {
t.Fatalf("WriteText: %v", err)
}
got, err := cb.ReadText(ctx)
if err != nil {
t.Fatalf("ReadText: %v", err)
}
if got != want {
t.Fatalf("ReadText = %q, ingin %q", got, want)
}
}Pisahkan file implementasi khusus OS dengan build constraints bila memang API-nya berbeda. Namun, jangan memakai build tag untuk menyembunyikan perbedaan perilaku yang seharusnya menjadi kontrak aplikasi. Bila macOS dan Windows sengaja memiliki semantik berbeda, dokumentasikan dan uji perbedaan itu secara eksplisit.
3. Puncak piramida: smoke test dan E2E GUI terbatas
Smoke test membuka aplikasi atau window, memastikan inisialisasi dasar berhasil, lalu menjalankan alur paling bernilai: misalnya membuat dokumen, mengubah state, menyimpan melalui adapter terkontrol, dan menutup aplikasi dengan bersih. Batasi jumlahnya. Tes ini mahal, paling rentan terhadap lingkungan, dan seharusnya membuktikan bahwa wiring native bekerja—bukan mengulang semua kombinasi aturan bisnis.
Jika toolkit atau arsitektur aplikasi menyediakan loop event yang dapat dikendalikan, gunakan hook sinkronisasi berbasis kondisi, misalnya sinyal bahwa window siap atau state tertentu telah dirender. Hindari asumsi seperti “tunggu 500 ms lalu klik”; durasi tersebut mungkin terlalu singkat pada runner lambat dan terlalu lama pada runner cepat.
Strategi CI per OS dan pemeriksaan konkurensi
Jalankan unit test serta contract test non-native pada setiap perubahan. Tambahkan job smoke test pada Windows, macOS, dan Linux sesuai platform yang didukung rilis. Linux runner GUI memerlukan sesi grafis yang sesuai dengan kebutuhan toolkit; jangan menganggap lingkungan headless memiliki perilaku identik dengan desktop pengguna.
go test ./...
go test -race ./...-race berguna untuk menemukan akses memori bersamaan yang sering muncul ketika callback native, goroutine worker, dan state UI berbagi data. Jalankan setidaknya pada platform CI yang didukung race detector dan pada paket yang relevan. Race detector bukan bukti bahwa desain threading sudah benar: tetap tetapkan satu pemilik state UI, lalu kirim hasil pekerjaan latar belakang melalui kanal atau dispatcher yang aman untuk event loop.
Pembagian pipeline yang praktis:
- Pull request: format, static analysis yang digunakan proyek, unit test, dan contract test fake di semua OS yang diperlukan.
- Job native per OS: build aplikasi dan smoke test singkat terhadap adapter atau startup native.
- Terjadwal atau sebelum rilis: smoke test lebih luas, tes visual terpilih, dan skenario yang membutuhkan resource desktop khusus.
Untuk kode khusus OS, pastikan job CI benar-benar mengompilasi serta menjalankan jalur tersebut. Kompilasi lintas target saja berguna untuk menangkap kesalahan build, tetapi tidak memvalidasi perilaku runtime integrasi native.
Timeout, observabilitas, dan artefak kegagalan
Timeout diperlukan agar CI tidak macet, tetapi harus berdasarkan batas operasi yang jelas. Gunakan context.WithTimeout pada operasi asynchronous atau tunggu kondisi dengan deadline. Saat timeout terjadi, laporkan state terakhir, event yang sudah diterima, serta operasi yang masih menunggu.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
select {
case result := <-saveDone:
if result.Err != nil {
t.Fatalf("simpan gagal: %v", result.Err)
}
case <-ctx.Done():
t.Fatalf("timeout menunggu simpan; state terakhir: %+v", model.State())
}Nilai timeout harus ditinjau dari data kegagalan CI, bukan dipilih secara acak. Menaikkan timeout dapat mengurangi gejala, tetapi tidak memperbaiki deadlock, event yang hilang, atau sinkronisasi yang salah.
Saat tes GUI gagal, unggah artefak yang membantu diagnosis:
- log terstruktur dengan OS, arsitektur, versi aplikasi, nama tes, action, dan transisi state;
- screenshot window atau area render terakhir jika framework memungkinkan;
- rekaman event/action ringkas untuk merekonstruksi urutan masalah;
- output build dan informasi lingkungan GUI yang relevan, tanpa membocorkan token atau data pengguna;
- dump goroutine jika proses berhenti merespons atau timeout.
Screenshot tidak selalu tersedia atau stabil pada runner headless. Dalam kondisi itu, log state dan jejak action sering lebih bernilai daripada gambar kosong atau gambar yang tidak dapat direproduksi.
Kapan visual regression test layak dipakai
Visual regression test membandingkan hasil render dengan baseline gambar. Gunakan ketika tampilan itu sendiri adalah kontrak penting, misalnya layout editor, panel data yang padat, status error yang harus terlihat, atau tema yang dikendalikan penuh oleh aplikasi. Tes ini kurang cocok untuk widget native yang tampilannya memang didelegasikan kepada OS, font yang tidak dipaketkan, animasi, konten waktu nyata, atau area yang dipengaruhi DPI dan GPU.
Agar bermanfaat, buat render deterministik: ukuran viewport tetap, data fixture tetap, locale dan zona waktu terkendali, animasi dimatikan atau dibekukan, serta font tersedia konsisten. Terapkan ambang perbedaan piksel hanya jika memahami konsekuensinya; ambang terlalu longgar dapat meloloskan kerusakan layout. Simpan baseline per platform bila perbedaan native memang diharapkan. Jangan paksa satu baseline untuk semua OS hanya demi menyederhanakan penyimpanan.
Workflow triage untuk flaky test
Flaky test adalah sinyal ketidakpastian: bisa pada produk, tes, atau infrastruktur. Menandainya sebagai retry-only tanpa penyelidikan membuat regresi nyata sulit dibedakan dari noise.
- Reproduksi: ambil log, seed data, screenshot, dan urutan action dari kegagalan. Jalankan tes yang sama berulang pada OS serta lingkungan yang sama. Jika hanya gagal di CI, cocokkan mode headless, scaling, locale, dan izin resource.
- Klasifikasi penyebab: bedakan antara race/data ownership, asumsi timing render, ketergantungan global seperti clipboard atau filesystem, perbedaan OS, resource CI yang tidak stabil, dan assertion visual yang terlalu sensitif.
- Perbaiki akar masalah: ganti sleep dengan sinyal kondisi, serialkan akses state UI, isolasi dependency global melalui adapter, buat fixture deterministik, atau pisahkan kontrak per OS bila semantiknya valid berbeda.
- Verifikasi: tambahkan tes regresi pada lapisan terendah yang mampu menangkap bug. Bila bug ada pada reducer, jangan hanya menambah E2E test. Jalankan berulang pada platform yang terdampak sebelum menghapus status flaky.
- Karantina secara terbatas: bila kegagalan memblokir tim dan perbaikan belum siap, nonaktifkan tes dengan tiket, pemilik, alasan, dan batas waktu. Tetap kumpulkan bukti kegagalan; retry otomatis hanyalah mitigasi sementara.
Checklist penerapan pada proyek GUI Go
- Pastikan state aplikasi dan aturan bisnis dapat diuji tanpa membuat window.
- Bungkus clipboard, dialog, filesystem khusus, dan layanan OS dalam adapter kecil.
- Gunakan fake untuk sebagian besar tes handler; jalankan contract test yang sama terhadap adapter native pada OS terkait.
- Jalankan smoke test startup dan alur kritis di setiap OS yang didukung.
- Gunakan race detector secara rutin pada paket yang melibatkan goroutine dan callback UI.
- Ganti penantian berbasis durasi dengan kondisi, channel, callback, atau context ber-deadline.
- Unggah log, screenshot bila relevan, dan dump goroutine sebagai artefak CI saat gagal.
- Tambahkan visual regression test hanya pada layar yang render-nya dapat dibuat deterministik dan bernilai tinggi.
Dengan struktur ini, pengujian pada proyek seperti Shirei berfokus pada risiko native yang benar: perbedaan platform diuji di adapter dan smoke test, sementara mayoritas perilaku aplikasi divalidasi cepat melalui state murni. Hasilnya adalah sinyal CI yang lebih dapat dipercaya dan investigasi regresi yang lebih singkat.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!