Sejauh ini kita menguji tiap fitur secara manual pakai curl. Bagus untuk belajar, tapi tidak scalable — setiap kali ada perubahan kode, kita harus mengulang semua skenario manual itu satu-satu. Di part ini kita tulis automated test: unit test untuk logic murni, dan feature test untuk endpoint HTTP end-to-end.
Unit Test: Validation
Go punya testing framework bawaan (testing package), tanpa perlu library eksternal. Buat validation/validation_test.go:
package validation
import "testing"
type sampleRequest struct {
Title string `validate:"required,min=3,max=10"`
Email string `validate:"required,email"`
}
func TestFormatErrors(t *testing.T) {
req := sampleRequest{Title: "ab", Email: "bukan-email"}
err := Validate.Struct(req)
if err == nil {
t.Fatal("expected validation error, got nil")
}
errs := FormatErrors(err)
if errs["Title"] != "Title minimal 3 karakter" {
t.Errorf("unexpected Title message: %q", errs["Title"])
}
if errs["Email"] != "Email harus berupa email yang valid" {
t.Errorf("unexpected Email message: %q", errs["Email"])
}
}
Jalankan dengan go test ./validation/.... Pola dasar testing di Go: nama fungsi diawali Test, menerima *testing.T, dan memanggil t.Error/t.Errorf (test lanjut jalan tapi ditandai gagal) atau t.Fatal/t.Fatalf (test langsung berhenti) kalau ada yang tidak sesuai ekspektasi.
Unit Test: JWT (Termasuk Regresi Keamanan)
Ingat celah algorithm confusion yang kita perbaiki di Part 8? Sekarang saatnya jadikan itu automated test, supaya kalau suatu saat ada perubahan kode yang tidak sengaja menghapus perlindungan itu, test akan gagal duluan sebelum sempat ter-deploy. Buat auth/jwt_test.go:
package auth
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
os.Setenv("JWT_SECRET", "test-secret-minimal-32-characters-long")
os.Exit(m.Run())
}
func TestGenerateAndParseAccessToken(t *testing.T) {
token, err := GenerateAccessToken(42, "admin")
if err != nil {
t.Fatalf("gagal generate token: %v", err)
}
claims, err := ParseAccessToken(token)
if err != nil {
t.Fatalf("gagal parse token: %v", err)
}
if claims.UserID != 42 {
t.Errorf("UserID = %d, want 42", claims.UserID)
}
if claims.Role != "admin" {
t.Errorf("Role = %q, want admin", claims.Role)
}
}
func TestParseAccessToken_RejectsForgedNoneAlgorithm(t *testing.T) {
// Token dengan header {"alg":"none","typ":"JWT"} dan signature kosong -
// simulasi serangan algorithm confusion yang sudah kita cegah di Part 8.
forged := "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0." +
"eyJ1c2VyX2lkIjoxLCJyb2xlIjoiYWRtaW4ifQ."
if _, err := ParseAccessToken(forged); err == nil {
t.Fatal("expected forged alg=none token to be rejected, but it was accepted")
}
}
TestMain di sini spesial — dijalankan sekali sebelum semua test lain di package yang sama, cocok untuk setup global (di sini: set JWT_SECRET supaya test tidak bergantung pada file .env).
Feature Test: Menguji Endpoint HTTP Sungguhan
Unit test bagus untuk logic terisolasi, tapi tidak menjamin routing, middleware, dan database saling terhubung dengan benar. Untuk itu kita pakai feature test — mengirim request HTTP asli ke aplikasi Fiber kita, lengkap dengan database sungguhan.
Fiber punya app.Test(req) yang menjalankan request tanpa perlu benar-benar membuka network port. Supaya bisa dipakai di test, kita ekstrak pembuatan *fiber.App dari main() ke fungsi terpisah. Update main.go:
func buildApp(db *gorm.DB) *fiber.App {
app := fiber.New(fiber.Config{ /* ...error handler seperti sebelumnya... */ })
app.Use(recover.New())
app.Static("/uploads", "./public/uploads")
app.Get("/", func(c *fiber.Ctx) error { /* ... */ })
routes.Setup(app, db)
return app
}
func main() {
// ...load env, connect db, migrate...
app := buildApp(db)
log.Fatal(app.Listen(":3000"))
}
Sekarang buildApp bisa dipanggil dari test tanpa harus benar-benar Listen di port manapun.
Database Khusus untuk Testing
Feature test butuh database sungguhan, tapi jangan pakai database development — test yang menghapus/mengubah data bisa merusak data yang sedang kita pakai untuk development manual. Buat database terpisah:
CREATE DATABASE blogfiber_dasar_test;
Buat .env.test.example sebagai referensi (mirip pola .env.example):
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=
DB_DATABASE=blogfiber_dasar_test
JWT_SECRET=test-secret-minimal-32-characters-long
Copy jadi .env.test dan sesuaikan kredensialnya (tambahkan juga .env.test ke .gitignore, sama seperti .env).
Setup Test & Reset Database
Buat main_test.go di root project:
package main
import (
"os"
"testing"
"github.com/joho/godotenv"
"gorm.io/gorm"
"blogfiber/database"
)
var testDB *gorm.DB
func TestMain(m *testing.M) {
if err := godotenv.Load(".env.test"); err != nil {
panic("gagal load .env.test - copy dari .env.test.example dan sesuaikan: " + err.Error())
}
testDB = database.Connect()
database.Migrate(testDB)
os.Exit(m.Run())
}
// resetDB mengosongkan semua tabel sebelum tiap test - urutan penghapusan
// mengikuti arah foreign key supaya tidak melanggar constraint.
func resetDB(t *testing.T) {
t.Helper()
tables := []string{"post_categories", "posts", "refresh_tokens", "categories", "users"}
for _, table := range tables {
if err := testDB.Exec("DELETE FROM " + table).Error; err != nil {
t.Fatalf("gagal reset table %s: %v", table, err)
}
}
}
Memanggil resetDB(t) di awal setiap test memastikan tiap test mulai dari kondisi database yang bersih dan tidak saling mempengaruhi satu sama lain — prinsip penting supaya test tetap dapat diandalkan (deterministic) walau dijalankan berkali-kali atau dalam urutan berbeda.
Menulis Feature Test
Buat helper untuk request JSON dan login, lalu tulis skenario end-to-end. Contoh untuk siklus CRUD Post lengkap (lihat kode lengkap di repo posts_feature_test.go):
func TestPostCRUD_HappyPath(t *testing.T) {
resetDB(t)
app := buildApp(testDB)
token := registerAndLogin(t, app, "budi@test.local", "rahasia123")
authHeader := "Bearer " + token
// Create
createReq := jsonRequest(t, http.MethodPost, "/api/v1/posts", map[string]interface{}{
"title": "Post Feature Test", "slug": "post-feature-test", "body": "isi post",
})
createReq.Header.Set("Authorization", authHeader)
createResp, _ := app.Test(createReq)
if createResp.StatusCode != http.StatusCreated {
t.Fatalf("create status = %d, want %d", createResp.StatusCode, http.StatusCreated)
}
// ...Show, Update, Delete, lalu Show lagi (harus 404)...
}
Test lain yang juga ditulis: TestPostCreate_RequiresAuth (tanpa token harus 401), TestPostUpdate_ForbiddenForNonOwner (user lain tidak boleh mengubah post orang lain, harus 403), dan TestCategoryCreate_RequiresAdmin (user biasa tidak boleh membuat kategori, harus 403) — masing-masing memverifikasi ulang aturan otorisasi yang sudah kita bangun sejak Part 9, 13, dan 16.
Menjalankan Semua Test
go test ./... -v
Semua test — unit maupun feature — akan berjalan, termasuk feature test yang benar-benar melakukan register, login, create/update/delete post ke database blogfiber_dasar_test. Kalau semua lolos, outputnya:
ok blogfiber 1.245s
ok blogfiber/auth 0.504s
ok blogfiber/validation 0.375s
Sekarang setiap kali ada perubahan kode, kita cukup jalankan go test ./... untuk memverifikasi seluruh alur autentikasi dan otorisasi masih berjalan benar — tidak perlu lagi mengulang semua skenario curl manual satu-satu. Di part berikutnya kita tambahkan dokumentasi API otomatis lewat Swagger/OpenAPI.
Bagian dari Series: GoFiber Dasar - Belajar Fundamental Lewat REST API Blog
Belajar konsep-konsep dasar GoFiber (routing, handler, GORM, MySQL, JWT auth) dengan membangun REST API blog sederhana dari nol - lengkap dengan auten...
Lihat Series Lengkap