Perubahan internal struct pada Go sering memicu breaking changes tanpa disadari. Mengubah tag JSON, menghapus field, atau mengganti tipe data primitif dapat merusak konsumsi data oleh client frontend maupun microservice lain jika spesifikasi OpenAPI publik tidak diperbarui secara bersamaan.
Artikel ini membahas teknik validasi kontrak API pada framework Go Fiber menggunakan pustaka kin-openapi. Pengujian dijalankan langsung melalui app.Test tanpa membuka port TCP, memungkinkan verifikasi skema OpenAPI v3 berlangsung cepat dalam pipeline Continuous Integration (CI).
Akar Masalah: Schema Drift antara Struct Go dan OpenAPI
Schema drift terjadi saat implementasi kode backend menyimpang dari dokumentasi kontrak yang disepakati. Pada ekosistem Go, akar masalah umumnya bersumber dari:
- Modifikasi Struct Tag: Pengembang mengubah
json:"user_id"menjadijson:"id"tanpa memperbarui dokumen OpenAPI. - Ketidaksesuaian Nullability: Penggunaan pointer (misal
*string) pada Go yang menghasilkannull, sementara skema OpenAPI mendefinisikannya sebagai tipe non-nullable. - Omit Empty Side-Effects: Tag
omitemptymenghilangkan field saat bernilai zero value, memicu kegagalan parsing pada client jika OpenAPI menandai field tersebut sebagairequired. - Generasi Dokumentasi Manual: OpenAPI dirawat terpisah tanpa mekanisme validasi otomatis di tingkat unit/integration test.
Solusi deterministik untuk masalah ini adalah contract testing: menjadikan dokumen OpenAPI sebagai sumber kebenaran (source of truth) dan memverifikasi response aktual dari Fiber handler secara langsung terhadap dokumen tersebut.
Arsitektur Pengujian: Fiber app.Test dan kin-openapi
Alih-alih menjalankan server HTTP nyata dengan app.Listen() yang memerlukan alokasi port dan overhead jaringan, Fiber menyediakan metode app.Test(). Metode ini memanfaatkan implementasi in-memory berbasis fasthttp, menerima objek *http.Request standar, dan mengembalikan *http.Response.
Untuk memvalidasi response tersebut terhadap dokumen OpenAPI v3, kita mengintegrasikan pustaka github.com/getkin/kin-openapi. Alur verifikasinya adalah:
- Memuat dokumen OpenAPI (YAML/JSON) menggunakan parser
kin-openapi. - Membuat router OpenAPI in-memory untuk memetakan rute pengujian ke operasi skema yang sesuai.
- Mengeksekusi handler via
app.Test(). - Mengonversi response Fiber ke input validasi
openapi3filteruntuk memeriksa status code, header, dan payload JSON.
Implementasi Kode Pengujian Kontrak
Berikut adalah implementasi pengujian kontrak. Pertama, definisikan spesifikasi kontrak minimal dalam format OpenAPI v3.
# openapi.yaml
openapi: 3.0.3
info:
title: User Service API
version: 1.0.0
paths:
/api/v1/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: User detail found
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
schemas:
UserResponse:
type: object
required:
- id
- email
properties:
id:
type: string
email:
type: string
format: email
bio:
type: string
nullable: true
ErrorResponse:
type: object
required:
- code
- message
properties:
code:
type: integer
message:
type: string
Selanjutnya, buat file pengujian Go yang memetakan rute Fiber dan mengeksekusi validasi kontrak.
package test
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers/gorillamux"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/require"
)
type UserPayload struct {
ID string `json:"id"`
Email string `json:"email"`
Bio *string `json:"bio"`
}
type ErrorPayload struct {
Code int `json:"code"`
Message string `json:"message"`
}
func setupFiberApp() *fiber.App {
app := fiber.New()
app.Get("/api/v1/users/:id", func(c *fiber.Ctx) error {
id := c.Params("id")
if id == "unknown" {
return c.Status(fiber.StatusNotFound).JSON(ErrorPayload{
Code: 404,
Message: "User not found",
})
}
bio := "Software Engineer"
return c.Status(fiber.StatusOK).JSON(UserPayload{
ID: id,
Email: "[email protected]",
Bio: &bio,
})
})
return app
}
func validateResponseContract(
t *testing.T,
ctx context.Context,
doc *openapi3.T,
httpReq *http.Request,
resp *http.Response,
) {
t.Helper()
router, err := gorillamux.NewRouter(doc)
require.NoError(t, err, "Gagal inisialisasi router OpenAPI")
route, pathParams, err := router.FindRoute(httpReq)
require.NoError(t, err, "Rute tidak terdefinisi di OpenAPI")
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
requestValidationInput := &openapi3filter.RequestValidationInput{
Request: httpReq,
PathParams: pathParams,
Route: route,
}
responseValidationInput := &openapi3filter.ResponseValidationInput{
RequestValidationInput: requestValidationInput,
Status: resp.StatusCode,
Header: resp.Header,
Body: io.NopCloser(bytes.NewReader(respBody)),
}
err = openapi3filter.ValidateResponse(ctx, responseValidationInput)
require.NoError(t, err, "Payload response melanggar kontrak OpenAPI")
}
func TestUsers_Contract(t *testing.T) {
ctx := context.Background()
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile("openapi.yaml")
require.NoError(t, err)
require.NoError(t, doc.Validate(ctx))
app := setupFiberApp()
t.Run("Success 200 - Valid Contract", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/usr-123", nil)
resp, err := app.Test(req, -1)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, fiber.StatusOK, resp.StatusCode)
validateResponseContract(t, ctx, doc, req, resp)
})
t.Run("Error 404 - Valid Contract", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/unknown", nil)
resp, err := app.Test(req, -1)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
validateResponseContract(t, ctx, doc, req, resp)
})
}
Menangani Field Opsional dan Error Response
Terdapat dua skenario kritis yang sering memunculkan false-positive atau false-negative saat validasi kontrak:
1. Field Opsional dan Nilai Null
Pada spesifikasi OpenAPI v3, sebuah properti dapat bersifat opsional (tidak tercantum di array required) atau bersifat nullable. Jika struct Go Anda mengembalikan null untuk field yang tidak memiliki deklarasi nullable: true di dokumen OpenAPI, openapi3filter akan melempar error:
// Error jika skema tidak mendefinisikan 'nullable: true'
response body doesn't match schema: value is not nullable
Pastikan penanganan pointer pada Go diselaraskan dengan atribut skema:
properties:
bio:
type: string
nullable: true # Mengizinkan string atau null
2. Status Code Error
Banyak pengujian hanya berfokus pada status 200 OK. Uji kontrak wajib mencakup skenario error (400, 404, 500). Kontrak harus menjamin format struktur error konsisten, sehingga client dapat melakukan error handling secara deterministik.
Integrasi CI Pipeline: Menolak Breaking Changes
Agar validasi ini efektif, uji kontrak harus dijalankan pada setiap pull request sebelum proses penggabungan kode (merge). Jika ada developer yang memodifikasi tipe data struct Go tanpa memperbarui OpenAPI, atau memperbarui OpenAPI dengan breaking change yang tidak sesuai handler, pipeline CI akan gagal secara otomatis.
Contoh konfigurasi GitHub Actions:
name: Contract Testing
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test-contract:
name: Verify API Contracts
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- name: Install Dependencies
run: go mod download
- name: Run Contract Tests
run: go test -v -race ./test/...
Kesimpulan dan Trade-Off
Mengintegrasikan kin-openapi dengan app.Test Fiber memberikan jaminan kepatuhan payload tanpa dependensi infrastruktur eksternal. Pendekatan ini menghilangkan kebutuhan untuk menjalankan mock server berbasis container atau tools eksternal seperti Prism di tahap testing awal.
Trade-off: Validasi skema runtime menambahkan waktu eksekusi pengujian mikrodetik lebih tinggi dibanding unit test standar. Namun, biaya komputasi ini sangat kecil dibandingkan risiko production outage akibat inkonsistensi payload antara backend dan client service.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!