M
Mr Sugiarto
Developer
04 Sep 2026 6 min read

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.

M
Mr Sugiarto

Developer

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