Mengapa Flaky Test Kerap Terjadi pada Pengujian Go Fiber
Flaky test adalah pengujian yang memberikan hasil tidak konsisten (kadang lolos, kadang gagal) tanpa adanya perubahan pada basis kode. Pada aplikasi backend berbasis Go Fiber, akar masalah pengujian tidak stabil ini umumnya bersumber dari tiga hal:
- Shared State Antar-Pengujian: Penggunaan satu instance
*fiber.Appglobal untuk seluruh suite pengujian menyebabkan route, middleware, dan state storage saling bertumpuk antar-skenario. - Race Condition pada Eksekusi Paralel (
t.Parallel()): Ketika runner pengujian Go menjalankan subtest secara bersamaan, goroutine yang membaca dan menulis data mock atau in-memory storage secara simultan memicu data race jika tidak diproteksi oleh mutual exclusion atau isolasi total. - Ketidakpastian Timeout pada Lingkungan CI: Mesin CI/CD sering kali memiliki resource CPU terbatas. Handler yang mengandalkan default timeout bawaan dapat mengalami batas waktu habis prematur saat antrean runner penuh.
Mekanisme Kerja app.Test: Menguji Tanpa Port TCP
Fiber menyediakan fungsi bawaan app.Test(*http.Request, ...int) (*http.Response, error). Fungsi ini tidak membuka socket TCP nyata dan tidak melakukan binding ke port jaringan (seperti :3000 atau :8080). Alih-alih melewati TCP stack sistem operasi, app.Test mengonversi http.Request standar menjadi fasthttp.Request, lalu mengeksekusinya langsung melalui pipeline routing Fiber di dalam memori.
Pendekatan ini memiliki keuntungan krusial:
- Eksekusi pengujian berjalan jauh lebih cepat karena tidak ada overhead handshake TCP.
- Menghilangkan galat "bind: address already in use" saat pengujian dijalankan paralel.
- Kompatibel dengan ekosistem standar Go karena menerima parameter
*http.Requestdari paketnet/httpdan mengembalikan*http.Responsestandar.
Konfigurasi Timeout app.Test
Secara default, app.Test menerapkan timeout sebesar 1.000 milidetik (1 detik). Jika handler mengeksekusi operasi asinkron yang kompleks atau runner CI mengalami penurunan performa, pengujian akan gagal dengan galat timeout. Teruskan nilai timeout eksplisit dalam satuan milidetik, atau gunakan nilai -1 untuk menonaktifkan timeout bawaan dan menyerahkan manajemen batas waktu ke context bawaan request (req.Context()):
// Menonaktifkan internal timeout Fiber, biarkan context request yang mengatur
resp, err := app.Test(req, -1)
// Atau tetapkan timeout eksplisit yang lebih longgar untuk pipeline CI
resp, err := app.Test(req, 5000) // 5 detik
Pola Desain Test Harness Terisolasi
Untuk memastikan determinisme, setiap unit test harus memiliki dependensi dan instance aplikasi yang berdiri sendiri. Pisahkan pembuatan router ke dalam fungsi pabrik (factory function) yang menerima interface dependensi, bukan struct konkret.
1. Deklarasi Interface dan Handler
Definisikan kontrak service agar dapat diganti dengan mock saat pengujian:
package user
import (
"context"
"github.com/gofiber/fiber/v2"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
type UserService interface {
GetUser(ctx context.Context, id string) (*User, error)
}
type Handler struct {
service UserService
}
func NewHandler(s UserService) *Handler {
return &Handler{service: s}
}
func (h *Handler) GetUserHandler(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.service.GetUser(c.UserContext(), id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
}
return c.Status(fiber.StatusOK).JSON(user)
}
2. Implementasi Factory dan Test Suite
Buat instance *fiber.App baru di dalam setiap eksekusi pengujian untuk mengisolasi state:
package user_test
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"path/to/user"
)
// MockUserService mengisolasi ketergantungan database eksternal
type MockUserService struct {
GetUserFn func(ctx context.Context, id string) (*user.User, error)
}
func (m *MockUserService) GetUser(ctx context.Context, id string) (*user.User, error) {
return m.GetUserFn(ctx, id)
}
// createTestApp mengembalikan instance Fiber baru yang terisolasi
func createTestApp(svc user.UserService) *fiber.App {
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
})
h := user.NewHandler(svc)
app.Get("/users/:id", h.GetUserHandler)
return app
}
func TestGetUserHandler(t *testing.T) {
t.Parallel()
tests := []struct {
name string
userID string
mockFn func(ctx context.Context, id string) (*user.User, error)
expectedStatus int
expectedBody string
}{
{
name: "Berhasil mengambil data user",
userID: "123",
mockFn: func(ctx context.Context, id string) (*user.User, error) {
return &user.User{ID: "123", Name: "Budi"}, nil
},
expectedStatus: http.StatusOK,
expectedBody: `{"id":"123","name":"Budi"}`,
},
{
name: "User tidak ditemukan",
userID: "999",
mockFn: func(ctx context.Context, id string) (*user.User, error) {
return nil, errors.New("not found")
},
expectedStatus: http.StatusNotFound,
expectedBody: `{"error":"user not found"}`,
},
}
for _, tt := range tests {
tt := tt // Pin variabel perulangan untuk go < 1.22
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mockService := &MockUserService{GetUserFn: tt.mockFn}
app := createTestApp(mockService)
req := httptest.NewRequest(http.MethodGet, "/users/"+tt.userID, nil)
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, 2000)
if err != nil {
t.Fatalf("Gagal mengeksekusi request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != tt.expectedStatus {
t.Errorf("Status tidak sesuai. Diharapkan: %d, Didapat: %d", tt.expectedStatus, resp.StatusCode)
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Gagal membaca response body: %v", err)
}
// Validasi payload JSON secara terkompresi tanpa terpengaruh whitespace
var actualMap, expectedMap map[string]interface{}
_ = json.Unmarshal(bodyBytes, &actualMap)
_ = json.Unmarshal([]byte(tt.expectedBody), &expectedMap)
actualJSON, _ := json.Marshal(actualMap)
expectedJSON, _ := json.Marshal(expectedMap)
if string(actualJSON) != string(expectedJSON) {
t.Errorf("Body payload tidak sesuai.\nDiharapkan: %s\nDidapat: %s", expectedJSON, actualJSON)
}
})
}
}
Aturan Mutlak Mengeliminasi Flaky Test
- Hindari Instance Global: Jangan pernah mendeklarasikan
var app *fiber.Appdi level package pengujian. Buat selalu instance baru di dalam setiap blokt.Run(). - Tutup Body Response: Selalu gunakan
defer resp.Body.Close()pada response yang dikembalikan olehapp.Testuntuk mencegah kebocoran resource buffer. - Validasi Data Race: Jalankan selalu suite pengujian dengan detektor race condition aktif menggunakan perintah:
go test -race -v ./... - Normalisasi JSON Assertion: Jangan membandingkan string JSON langsung (raw string comparison) karena urutan key JSON pada struct map tidak bersifat deterministik. Selalu lakukan unmarshal ke tipe data Go sebelum memeriksa nilainya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!