Contract test API berguna ketika beberapa layanan saling bergantung dan perubahan kecil pada request atau response bisa memicu regresi di production. Pada konteks Go Fiber, contract testing membantu memastikan bahwa provider tetap memenuhi ekspektasi consumer tanpa harus selalu menjalankan integration test end-to-end yang mahal dan rapuh.
Intinya, contract test memverifikasi kesepakatan antara client dan server: path, method, header penting, struktur payload, tipe data, field wajib, hingga kode status. Dengan pendekatan ini, tim bisa mendeteksi breaking change lebih cepat di pipeline CI sebelum perubahan API dirilis.
Kapan contract test lebih tepat dibanding unit test atau integration test
Contract test bukan pengganti semua jenis test. Ia efektif untuk area yang berada di batas antarlayanan, terutama ketika banyak service mengonsumsi API yang sama.
Unit test
Unit test cocok untuk memverifikasi logika internal: validasi input, transformasi data, branching, atau fungsi domain. Unit test cepat dan presisi, tetapi tidak cukup untuk menjamin bahwa payload HTTP yang diterima consumer tetap kompatibel.
Integration test
Integration test cocok untuk memverifikasi koneksi nyata antar komponen seperti HTTP server, database, cache, atau message broker. Namun, integration test cenderung lebih lambat, lebih sulit dipelihara, dan sering gagal karena masalah environment, bukan karena kontrak API benar-benar berubah.
Contract test
Contract test lebih tepat saat kebutuhan utamanya adalah:
- Memastikan response API tetap sesuai kebutuhan consumer.
- Mencegah penghapusan atau perubahan field yang diam-diam merusak klien lama.
- Memvalidasi bahwa request yang dikirim consumer masih diterima provider.
- Memberi umpan balik cepat di CI tanpa harus menyalakan seluruh environment end-to-end.
Praktiknya, kombinasi yang sehat biasanya adalah:
- Unit test untuk logika internal.
- Contract test untuk batas antar service.
- Sejumlah kecil integration/end-to-end test untuk jalur kritis.
Bentuk producer-consumer contract
Dalam arsitektur layanan, ada dua peran utama:
- Provider: layanan yang menyediakan API.
- Consumer: layanan atau aplikasi yang memakai API tersebut.
Contract bisa ditulis dalam beberapa bentuk, misalnya:
- Dokumen skema JSON untuk request dan response.
- Spesifikasi OpenAPI yang memuat bentuk endpoint.
- File contract yang dibuat dari ekspektasi consumer, lalu diverifikasi oleh provider.
Yang paling penting bukan formatnya, tetapi isi kontraknya. Minimal, kontrak sebaiknya mencakup:
- HTTP method dan path.
- Query parameter atau path parameter penting.
- Header yang wajib.
- Struktur request body.
- Struktur response body.
- Kode status yang diharapkan.
- Field wajib vs field opsional.
- Tipe data dan batas nilai bila relevan.
Kesalahan umum adalah menganggap semua field harus identik 100%. Contract test sebaiknya fokus pada field yang memang dibutuhkan consumer, bukan mengunci seluruh response jika tidak perlu. Ini mengurangi test yang rapuh.
Contoh struktur proyek Go untuk contract test
Berikut contoh struktur proyek yang sederhana tetapi cukup realistis untuk Go Fiber:
project/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── delivery/
│ │ └── http/
│ │ ├── handler_user.go
│ │ └── router.go
│ ├── domain/
│ │ └── user.go
│ └── service/
│ └── user_service.go
├── contracts/
│ ├── consumer/
│ │ └── user_get_by_id_contract.json
│ └── schemas/
│ ├── user_response_v1.json
│ └── error_response.json
├── tests/
│ ├── contract/
│ │ ├── consumer_test.go
│ │ └── provider_test.go
│ └── integration/
└── go.modPemisahan ini membantu karena:
internal/menampung implementasi aplikasi.contracts/menampung artefak kontrak dan skema yang dapat ditinjau di pull request.tests/contract/memuat verifikasi consumer dan provider secara terpisah.
Contoh endpoint API di Go Fiber
Misalkan provider memiliki endpoint GET /v1/users/:id. Consumer mengandalkan field tertentu: id, name, dan email.
package http
import (
"github.com/gofiber/fiber/v2"
)
type UserService interface {
GetByID(id string) (*UserResponse, error)
}
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type UserHandler struct {
service UserService
}
func NewUserHandler(service UserService) *UserHandler {
return &UserHandler{service: service}
}
func (h *UserHandler) GetByID(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.service.GetByID(id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": "user not found",
})
}
return c.Status(fiber.StatusOK).JSON(user)
}Router Fiber:
package http
import "github.com/gofiber/fiber/v2"
func RegisterRoutes(app *fiber.App, userHandler *UserHandler) {
v1 := app.Group("/v1")
v1.Get("/users/:id", userHandler.GetByID)
}Endpoint di atas terlihat sederhana, tetapi justru di sinilah regresi sering terjadi: field diubah namanya, tipe data bergeser, atau kode status error berubah tanpa koordinasi dengan consumer.
Mendefinisikan contract untuk request dan response
Salah satu pendekatan praktis adalah menyimpan kontrak response dalam bentuk JSON schema sederhana. Tujuannya bukan membangun teori skema yang kompleks, melainkan memberi aturan yang bisa diverifikasi otomatis.
Contoh contract response v1
{
"type": "object",
"required": ["id", "name", "email"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"email": { "type": "string" }
},
"additionalProperties": true
}Pilihan additionalProperties: true adalah keputusan penting. Dengan ini, provider boleh menambah field baru tanpa merusak consumer lama, selama field yang dibutuhkan tetap ada dan tipenya tidak berubah.
Kapan field baru aman ditambahkan
Menambahkan field response biasanya aman jika:
- Consumer tidak memerlukan response yang identik persis.
- Field lama tetap ada.
- Tipe data field lama tidak berubah.
Sebaliknya, perubahan berikut biasanya breaking change:
- Menghapus field yang sebelumnya wajib.
- Mengganti tipe data, misalnya
iddari string menjadi number. - Mengganti nama field tanpa masa transisi.
- Mengubah kode status sukses atau error yang dipakai consumer untuk branching.
Contoh consumer contract test
Di sisi consumer, test mendeskripsikan ekspektasi minimum terhadap provider. Consumer tidak perlu menguji seluruh implementasi provider; yang diuji adalah bentuk interaksi yang dibutuhkan.
package contract
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
)
type ConsumerExpectedUser struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func TestConsumerExpectation_GetUserByID(t *testing.T) {
app := fiber.New()
app.Get("/v1/users/:id", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"id": "u-123",
"name": "Ayu",
"email": "[email protected]",
"role": "admin",
})
})
req := httptest.NewRequest(http.MethodGet, "/v1/users/u-123", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status: %d", resp.StatusCode)
}
var body ConsumerExpectedUser
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.ID == "" || body.Name == "" || body.Email == "" {
t.Fatal("required fields are missing")
}
}Meski contoh ini sederhana, idenya jelas: consumer hanya menuntut field yang benar-benar dipakai. Field tambahan seperti role tidak membuat test gagal.
Contoh provider verification test di Go Fiber
Di sisi provider, kita ingin memverifikasi bahwa implementasi server saat ini masih memenuhi kontrak yang telah disepakati. Anda bisa memuat skema lalu memeriksa response endpoint nyata dari app Fiber.
package contract
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
fiberhttp "project/internal/delivery/http"
"github.com/gofiber/fiber/v2"
)
type mockUserService struct{}
func (m *mockUserService) GetByID(id string) (*fiberhttp.UserResponse, error) {
return &fiberhttp.UserResponse{
ID: id,
Name: "Ayu",
Email: "[email protected]",
}, nil
}
func TestProviderContract_GetUserByID(t *testing.T) {
app := fiber.New()
handler := fiberhttp.NewUserHandler(&mockUserService{})
fiberhttp.RegisterRoutes(app, handler)
req := httptest.NewRequest(http.MethodGet, "/v1/users/u-123", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status: %d", resp.StatusCode)
}
var body map[string]any
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatal(err)
}
requiredFields := []string{"id", "name", "email"}
for _, field := range requiredFields {
if _, ok := body[field]; !ok {
t.Fatalf("missing required field: %s", field)
}
}
if _, ok := body["id"].(string); !ok {
t.Fatal("field id must be string")
}
}Untuk sistem yang lebih besar, validasi dapat dihubungkan ke file contract atau schema sehingga provider test memeriksa implementasi terhadap artefak kontrak, bukan hardcoded assertion di banyak tempat.
Workflow verifikasi contract yang praktis
Agar contract testing benar-benar mencegah regresi, workflow-nya harus jelas. Salah satu pola yang paling masuk akal adalah consumer-driven contract.
Alur consumer-driven contract
- Consumer mendefinisikan kebutuhan API minimum dalam contract.
- Contract disimpan sebagai artefak yang bisa dibaca tim provider.
- Provider menjalankan verification test terhadap contract itu di CI.
- Perubahan provider hanya boleh dirilis jika seluruh contract consumer yang relevan lolos.
Keuntungan pendekatan ini adalah provider tidak menebak-nebak field mana yang penting. Contract datang langsung dari kebutuhan nyata consumer.
Alur pull request yang disarankan
- Developer mengubah endpoint di provider atau kebutuhan payload di consumer.
- Contract ikut diperbarui jika memang ada perubahan perilaku API.
- CI consumer menjalankan test untuk memastikan contract baru valid dari sudut pandang consumer.
- CI provider menjalankan verifikasi terhadap semua contract yang aktif.
- Jika ada breaking change, merge ditahan sampai ada strategi kompatibilitas atau versi baru API.
Jika organisasi Anda belum siap dengan broker contract khusus, mulai saja dari file contract di repository dan verifikasi otomatis di CI. Proses yang sederhana tetapi konsisten lebih berguna daripada tooling yang kompleks namun tidak dipakai.
Versi skema payload dan backward compatibility
Contract testing paling bernilai saat dipakai untuk mengelola perubahan API secara disiplin. Masalah terbesar biasanya bukan menambah endpoint baru, melainkan mengubah payload yang sudah dikonsumsi banyak layanan.
Aturan sederhana backward compatibility
- Aman: menambah field response opsional.
- Aman dengan hati-hati: menambah query parameter opsional atau header opsional.
- Berisiko: mengubah format string, misalnya tanggal atau enum, walau nama field tetap sama.
- Breaking: menghapus field, mengganti tipe, mengganti nama, atau menjadikan field opsional lama sebagai wajib pada request.
Strategi versi payload
Anda tidak selalu perlu membuat /v2 untuk setiap perubahan. Gunakan versi baru hanya saat backward compatibility memang tidak bisa dipertahankan. Sebelum itu, pertimbangkan strategi berikut:
- Tambahkan field baru sambil mempertahankan field lama selama masa transisi.
- Dukung dua bentuk payload sementara jika migrasi consumer butuh waktu.
- Tandai field lama sebagai deprecated dalam dokumentasi dan contract.
- Hapus field lama hanya setelah seluruh consumer tervalidasi sudah tidak bergantung padanya.
Contoh validasi backward compatibility
Misalkan provider ingin menambahkan full_name dan suatu hari menghapus name. Langkah aman:
- Rilis
full_nametanpa menghapusname. - Perbarui consumer agar bisa membaca
full_nameatau tetap memakainameselama transisi. - Jalankan contract verification untuk memastikan consumer lama masih lulus.
- Setelah semua consumer berpindah, baru jadwalkan penghapusan di versi API baru atau fase deprecation yang jelas.
Yang perlu dihindari adalah penghapusan field langsung di branch provider hanya karena unit test internal masih hijau. Unit test tidak tahu ada layanan lain yang membaca field tersebut.
Integrasi ke CI untuk reliability rilis
Nilai contract test akan turun jika hanya dijalankan manual. Agar benar-benar mencegah regresi, verifikasi harus menjadi bagian dari pipeline build.
Contoh langkah CI
- Install dependency Go.
- Jalankan unit test.
- Jalankan contract test consumer.
- Jalankan provider verification test.
- Gagal-kan pipeline jika ada contract yang tidak kompatibel.
Contoh perintah yang umum:
go test ./...
go test ./tests/contract/...Jika contract disimpan sebagai file, pastikan file tersebut ikut ditinjau saat pull request. Review kontrak sering kali lebih mudah dipahami daripada review perubahan implementasi handler yang panjang.
Gate rilis yang disarankan
- Jangan rilis provider jika verification terhadap contract consumer aktif gagal.
- Jangan merge consumer yang membutuhkan perubahan API baru sebelum provider siap memenuhinya, kecuali ada feature flag atau fallback.
- Simpan histori contract agar tim bisa melacak kapan kompatibilitas berubah.
Jebakan umum saat menerapkan contract test
1. Contract terlalu ketat
Jika contract mengharuskan response identik persis termasuk semua field tambahan, test akan sering gagal untuk perubahan yang sebenarnya aman. Fokuskan contract pada hal yang benar-benar dibutuhkan consumer.
2. Contract terlalu longgar
Sebaliknya, jika hanya memeriksa status 200 tanpa memverifikasi field penting, test menjadi tidak berguna. Pastikan field wajib, tipe data, dan error utama tetap diperiksa.
3. Hanya menguji happy path
Consumer sering bergantung pada perilaku error: 404 saat resource tidak ada, 400 untuk input salah, atau 401/403 untuk otorisasi. Jika branch error tidak diuji, regresi tetap bisa lolos.
4. Mengabaikan request contract
Banyak tim hanya memvalidasi response. Padahal request juga bagian dari kontrak: field wajib, format body, header, query parameter, dan batas validasi.
5. Tidak jelas siapa pemilik contract
Contract tanpa kepemilikan akan cepat basi. Tetapkan apakah consumer yang mendefinisikan kebutuhan, provider yang menyetujui, dan siapa yang berwenang menandai perubahan sebagai breaking.
Tips debugging saat contract test gagal
- Bandingkan payload aktual vs payload kontrak, bukan hanya status test gagal.
- Periksa perubahan tag JSON pada struct Go, karena rename kecil bisa langsung mengubah field publik.
- Periksa penggunaan
omitemptyjika field tiba-tiba hilang pada response. - Pastikan mock service di provider test merepresentasikan perilaku nyata, bukan menyembunyikan masalah serialisasi.
- Cek kode status error; regresi sering muncul di middleware, bukan di handler utama.
- Jika payload memiliki tanggal atau enum, pastikan formatnya konsisten dan tidak berubah diam-diam.
Checklist agar perubahan API tidak merusak klien lama
- Apakah field yang dibutuhkan consumer lama masih ada?
- Apakah tipe data field lama tetap sama?
- Apakah kode status sukses dan error tetap kompatibel?
- Apakah field request yang dulu opsional tidak berubah menjadi wajib tanpa versi baru?
- Apakah field baru benar-benar opsional bagi consumer lama?
- Apakah contract test untuk happy path dan error path sudah ada?
- Apakah provider verification dijalankan di CI sebelum rilis?
- Apakah perubahan breaking sudah diberi jalur migrasi atau versi API baru?
- Apakah dokumentasi dan contract diperbarui bersamaan dengan kode?
Penutup
Penerapan contract test API pada Go Fiber memberi perlindungan yang sangat praktis terhadap regresi antar layanan. Pendekatan ini paling efektif ketika dipakai untuk memverifikasi batas antarsistem: request, response, status code, dan kompatibilitas payload dari waktu ke waktu.
Mulailah dengan contract yang kecil tetapi jelas pada endpoint kritis, jalankan verifikasi di CI, dan fokus pada backward compatibility. Dengan begitu, tim bisa merilis perubahan API dengan lebih percaya diri tanpa mengorbankan klien lama yang masih bergantung pada kontrak sebelumnya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!