Contract test API dipakai untuk memastikan kesepakatan antara penyedia API (producer) dan pemakai API (consumer) tetap konsisten dari waktu ke waktu. Jika ada perubahan pada response, request, status code, atau bentuk error yang mematahkan asumsi consumer, regression bisa terdeteksi sebelum rilis, bukan setelah service saling memanggil di environment bersama.

Masalah yang sering terjadi bukan karena API benar-benar down, tetapi karena kontraknya diam-diam berubah: field wajib dihapus, enum bertambah tanpa penanganan, status code berubah dari 200 ke 204, atau format error tidak lagi sama. Unit test biasanya terlalu lokal, sedangkan integration test penuh cenderung lambat, rapuh, dan mahal dirawat. Di sinilah contract test API memberi nilai paling jelas.

Kapan Contract Test API Lebih Tepat daripada Unit Test atau Integration Test

Contract test bukan pengganti semua jenis test. Ia mengisi celah spesifik: apakah producer dan consumer masih sepakat soal antarmuka yang dipertukarkan.

Unit test cukup ketika

  • Anda hanya memverifikasi logika internal fungsi atau class.
  • Tidak ada ketergantungan lintas service yang signifikan.
  • Format payload eksternal tidak menjadi fokus.

Integration test lebih tepat ketika

  • Anda perlu memastikan koneksi nyata ke database, message broker, cache, atau service lain.
  • Anda menguji alur sistem end-to-end.
  • Perlu memverifikasi wiring, autentikasi, network, timeout, atau konfigurasi runtime.

Contract test API paling tepat ketika

  • Ada beberapa consumer yang bergantung pada endpoint yang sama.
  • Producer dan consumer dikembangkan oleh tim berbeda.
  • Perubahan kecil pada JSON response bisa mematahkan frontend atau service lain.
  • Integration test penuh terlalu lambat atau sulit distabilkan.
  • Anda ingin validasi kompatibilitas di CI sebelum deployment.

Singkatnya: unit test memeriksa logika, integration test memeriksa interaksi nyata, sedangkan contract test memeriksa kesepakatan antarmuka.

Jenis Kontrak yang Perlu Dijaga

Kontrak API tidak hanya berarti "schema JSON ada". Dalam praktik, ada beberapa aspek yang perlu dijaga agar consumer tidak rusak diam-diam.

1. Schema dan tipe data

Pastikan struktur payload konsisten: object, array, string, number, boolean, nullable, dan nested field. Perubahan tipe data adalah sumber breaking change yang umum, misalnya id dari number menjadi string tanpa koordinasi.

2. Field wajib dan optional

Consumer sering bergantung pada field tertentu. Jika field wajib dihapus, di-rename, atau menjadi optional tanpa fallback, bug akan muncul. Sebaliknya, penambahan field baru umumnya aman selama consumer tidak memaksa strict equality terhadap seluruh payload.

3. Enum atau nilai terbatas

Field seperti status, role, atau payment_method sering dianggap stabil. Menambah nilai enum baru bisa mematahkan consumer yang hanya mengenali beberapa nilai dan tidak punya default handling.

4. Status code

Perubahan dari 200 ke 201, 204, atau 202 mungkin terlihat kecil, tetapi bisa merusak client yang mengandalkan body response tertentu. Hal yang sama berlaku pada perubahan perilaku error dari 404 menjadi 400.

5. Error shape

Banyak tim hanya menguji happy path. Padahal consumer sering bergantung pada struktur error untuk menampilkan pesan, retry, atau logging. Kontrak error minimal biasanya mencakup kode error, pesan, dan detail validasi.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "email is invalid",
    "details": {
      "field": "email"
    }
  }
}

6. Aturan semantik penting

Tidak semua kontrak bisa diekspresikan sebagai schema. Misalnya:

  • Jika status=cancelled, maka cancelled_at harus ada.
  • Array boleh kosong, tetapi tidak boleh null.
  • Field tanggal harus format ISO-8601.

Contract test yang baik biasanya menggabungkan pengecekan schema dengan beberapa aturan semantik penting seperti ini.

Contoh Breaking Change pada Endpoint JSON

Misalkan ada endpoint producer:

GET /orders/123

Response awal:

{
  "id": "123",
  "status": "paid",
  "total": 150000,
  "currency": "IDR",
  "customer": {
    "id": "c-01",
    "name": "Rina"
  }
}

Frontend consumer memakai status untuk badge, total untuk tampilan harga, dan customer.name untuk label.

Lalu producer melakukan refactor dan mengubah response menjadi:

{
  "id": "123",
  "state": "paid",
  "amount": {
    "value": 150000,
    "currency": "IDR"
  },
  "customer": {
    "id": "c-01",
    "full_name": "Rina"
  }
}

Dari sisi producer, perubahan ini mungkin masuk akal. Dari sisi consumer, ini jelas breaking karena:

  • status diubah menjadi state
  • total tidak ada lagi
  • currency dipindahkan
  • customer.name diubah menjadi customer.full_name

Unit test di service producer bisa tetap hijau. Integration test producer ke database juga bisa tetap lolos. Namun contract test API akan gagal karena bentuk respons tidak lagi memenuhi kontrak consumer.

Cara Menyusun Producer dan Consumer Contract

Ada dua pendekatan umum: consumer-driven contract dan provider-owned schema. Untuk tim kecil, mulai dari bentuk yang paling sederhana dan eksplisit lebih penting daripada memilih tooling paling canggih.

Pendekatan 1: Consumer-driven contract

Consumer mendefinisikan interaksi minimum yang ia butuhkan. Producer lalu memverifikasi bahwa implementasinya masih memenuhi kontrak tersebut.

Contoh kontrak sederhana dalam bentuk JSON Schema-like assertion:

{
  "request": {
    "method": "GET",
    "path": "/orders/123"
  },
  "response": {
    "status": 200,
    "body": {
      "id": "string",
      "status": ["paid", "pending", "cancelled"],
      "total": "number",
      "currency": "string",
      "customer": {
        "id": "string",
        "name": "string"
      }
    },
    "required": ["id", "status", "total", "currency", "customer"]
  }
}

Keuntungan pendekatan ini:

  • Fokus pada kebutuhan nyata consumer, bukan seluruh payload.
  • Mencegah producer membuat perubahan yang mematahkan pemakaian aktual.
  • Cocok untuk banyak consumer dengan kebutuhan berbeda.

Kekurangannya:

  • Kontrak bisa tersebar jika consumer banyak.
  • Producer harus memverifikasi banyak kontrak.
  • Jika consumer menulis kontrak terlalu detail, test jadi rapuh.

Pendekatan 2: Producer-owned schema

Producer menerbitkan schema atau spesifikasi API, misalnya OpenAPI atau JSON Schema. Consumer memvalidasi bahwa payload yang dipakai sesuai schema tersebut.

Keuntungan:

  • Satu sumber dokumentasi dan validasi.
  • Cocok jika API bersifat platform dan producer mengontrol evolusinya.
  • Mudah dipakai untuk linting dan validasi request/response.

Kekurangan:

  • Schema bisa terlalu umum dan tidak menangkap kebutuhan consumer tertentu.
  • Tidak otomatis menjamin bahwa contoh penggunaan consumer tetap aman.

Untuk banyak tim, kombinasi keduanya paling realistis: producer punya schema umum, consumer menambah contract test untuk bagian yang benar-benar dipakai.

Apa yang sebaiknya diuji dalam kontrak

  • Endpoint dan method yang dipakai consumer
  • Status code yang diharapkan
  • Header penting bila relevan, misalnya content type
  • Field wajib yang benar-benar dipakai consumer
  • Enum dan format data yang sensitif
  • Shape error untuk kasus yang ditangani consumer

Hindari memasukkan field yang tidak dipakai consumer hanya karena tersedia di response. Semakin banyak detail yang tidak penting dimasukkan, semakin besar peluang false positive.

Workflow Verifikasi Contract Test API di CI

Tujuan utama contract test API di CI adalah mendeteksi inkompatibilitas sebelum merge atau deploy. Workflow minimal yang praktis biasanya seperti ini:

  1. Consumer menulis atau memperbarui kontrak berdasarkan kebutuhan endpoint tertentu.
  2. Consumer menjalankan test lokal terhadap stub/mock yang sesuai kontrak.
  3. Kontrak dipublikasikan ke repositori bersama, folder khusus, atau artifact CI.
  4. Producer mengambil kontrak terbaru dan menjalankan verifikasi terhadap implementasi aktual atau test harness.
  5. Jika verifikasi gagal, perubahan diblokir sampai ada perbaikan atau kesepakatan perubahan kontrak.

Contoh alur sederhana tanpa tooling kompleks

Misalkan tim belum memakai broker kontrak khusus. Anda bisa mulai dengan struktur seperti ini:

contracts/
  web-frontend/
    get-order.json
  billing-service/
    get-order.json

Di pipeline consumer:

# pseudo-command
npm test
cp contracts/web-frontend/get-order.json build-artifacts/contracts/

Di pipeline producer:

# pseudo-command
run-api-in-test-mode
verify-contracts ./build-artifacts/contracts

Verifikasi bisa berupa test yang memanggil handler lokal, test HTTP terhadap instance ephemeral, atau validasi response terhadap schema kontrak.

Kapan verifikasi dijalankan

  • Di pull request consumer: memastikan kontrak yang ditulis masuk akal.
  • Di pull request producer: memastikan perubahan producer tidak merusak kontrak yang sudah ada.
  • Sebelum deploy: terutama jika ada banyak service yang bergantung satu sama lain.

Untuk tim kecil, prioritas terbaik biasanya verifikasi di CI producer. Itu titik di mana breaking change paling sering masuk.

Strategi Versioning Kontrak

Versioning kontrak bukan sekadar memberi label v1 dan v2. Tujuannya adalah mengelola perubahan kompatibel dan inkompatibel dengan jelas.

Perubahan yang umumnya backward-compatible

  • Menambah field baru yang optional
  • Menambah endpoint baru
  • Menambah metadata yang tidak diwajibkan consumer

Perubahan yang biasanya breaking

  • Menghapus field yang dipakai consumer
  • Mengubah nama field
  • Mengubah tipe data
  • Mengubah status code atau error shape tanpa koordinasi
  • Memperketat validasi request yang sebelumnya diterima

Pola versioning yang realistis

  • Versi endpoint: misalnya /v1/orders dan /v2/orders. Mudah dipahami, tetapi bisa menggandakan maintenance.
  • Versi kontrak di repository: setiap perubahan kontrak dilacak sebagai file dan review code biasa.
  • Compat window: producer mendukung kontrak lama dan baru selama periode transisi.

Jika perubahan memang breaking dan disengaja, jangan langsung mengganti kontrak lama. Tambahkan kontrak baru, biarkan consumer bermigrasi, lalu hapus kontrak lama setelah semua consumer siap.

Anti-pattern yang umum adalah menganggap penambahan enum selalu aman. Secara teknis schema mungkin valid, tetapi secara bisnis consumer bisa salah menangani nilai baru itu.

Menangani False Positive dan Perubahan yang Disengaja

Contract test yang terlalu ketat akan sering gagal padahal perubahan tidak benar-benar mematahkan consumer. Ini menurunkan kepercayaan tim terhadap suite test.

Penyebab false positive yang umum

  • Consumer mengunci seluruh payload, termasuk field yang tidak dipakai
  • Urutan field JSON dianggap penting
  • Nilai contoh dianggap nilai tetap, padahal seharusnya hanya tipe atau pola
  • Timestamp, ID, atau data dinamis tidak dimatch dengan aturan yang tepat

Cara mengurangi false positive

  • Uji hanya field yang menjadi kebutuhan consumer
  • Gunakan matcher berbasis tipe, pola, atau enum, bukan literal yang terlalu spesifik
  • Pisahkan kontrak happy path dan error path yang benar-benar relevan
  • Jangan memaksa exact match untuk field tambahan yang bersifat optional

Contoh matcher yang lebih baik secara konsep:

{
  "id": "any-string",
  "status": "one-of: paid|pending|cancelled",
  "total": "any-number",
  "created_at": "iso-8601-datetime"
}

Jika perubahan memang disengaja

  1. Tandai perubahan sebagai breaking di pull request.
  2. Diskusikan consumer mana yang terdampak.
  3. Tambah kontrak baru atau versi baru, jangan langsung menghapus yang lama.
  4. Jalankan verifikasi untuk kontrak lama dan baru selama masa transisi.
  5. Hapus kontrak lama setelah semua consumer bermigrasi.

Dengan cara ini, contract test tetap menjadi alat koordinasi teknis, bukan sekadar penghalang CI.

Trade-off, Batasan, dan Anti-Pattern

Trade-off utama

  • Lebih cepat dari integration test penuh, tetapi tidak menggantikan verifikasi runtime seperti network timeout, auth, dan konfigurasi environment.
  • Lebih fokus dari end-to-end test, tetapi tetap menambah biaya maintenance kontrak.
  • Lebih aman untuk evolusi API, tetapi butuh disiplin lintas tim.

Batasan contract test

  • Tidak membuktikan seluruh sistem bekerja end-to-end.
  • Tidak otomatis menangkap masalah performa atau kapasitas.
  • Tidak cukup untuk menguji query database, transactional boundary, atau side effect kompleks.
  • Tidak menjamin semantik bisnis jika kontrak hanya memeriksa shape payload.

Anti-pattern yang perlu dihindari

  • Menguji seluruh response secara kaku: membuat perubahan kecil non-breaking terlihat seperti masalah besar.
  • Mengandalkan mock tanpa verifikasi producer: consumer merasa aman, padahal producer tidak pernah benar-benar diverifikasi.
  • Menulis kontrak dari dokumentasi saja: kontrak harus mencerminkan kebutuhan nyata consumer atau perilaku aktual producer.
  • Tidak menguji error shape: bug sering muncul justru saat validasi gagal atau resource tidak ditemukan.
  • Menganggap contract test cukup sebagai satu-satunya pengaman: tetap butuh unit test, integration test, dan observability.

Checklist Implementasi Bertahap untuk Tim Kecil

Anda tidak perlu langsung mengadopsi platform contract testing yang kompleks. Mulailah dari endpoint paling kritis dan workflow yang sederhana.

Tahap 1: Pilih scope sempit

  • Pilih 1-3 endpoint yang paling sering menyebabkan regression.
  • Identifikasi consumer yang benar-benar memakai endpoint itu.
  • Tentukan field wajib, enum, status code, dan error shape yang penting.

Tahap 2: Tulis kontrak minimum

  • Fokus pada kebutuhan consumer, bukan seluruh payload.
  • Definisikan matcher untuk tipe dan pola data dinamis.
  • Tambahkan minimal satu skenario error.

Tahap 3: Verifikasi di CI producer

  • Ambil kontrak dari repository atau artifact.
  • Jalankan test verifikasi pada setiap pull request.
  • Gagal-kan pipeline jika ada breaking change yang belum dikoordinasikan.

Tahap 4: Tambahkan aturan perubahan

  • Dokumentasikan mana perubahan compatible dan mana yang breaking.
  • Tetapkan prosedur transisi untuk perubahan disengaja.
  • Review kontrak seperti review code biasa.

Tahap 5: Rapikan observability dan debugging

  • Simpan diff kontrak saat verifikasi gagal.
  • Tampilkan field mana yang hilang, berubah tipe, atau berubah status code.
  • Kaitkan hasil gagal dengan commit dan pull request yang memicu perubahan.

Tips Debugging saat Verifikasi Gagal

  • Periksa dulu apakah yang gagal benar-benar field yang dipakai consumer.
  • Bandingkan response aktual dan kontrak dalam bentuk diff yang mudah dibaca.
  • Pastikan data fixture atau stub tidak sudah usang.
  • Cek apakah kegagalan berasal dari perubahan status code atau error shape, bukan hanya body sukses.
  • Validasi apakah perubahan enum baru memerlukan fallback di consumer.
  • Jika producer memakai serializer atau mapper, audit perubahan di layer itu karena sering menjadi sumber regression.

Penutup

Contract test API untuk cegah regression antar service paling efektif saat Anda ingin menjaga kompatibilitas antarmuka tanpa harus selalu menjalankan integration test end-to-end yang mahal. Fokus utamanya adalah kesepakatan yang benar-benar dipakai consumer: schema, field wajib, enum, status code, dan error shape.

Mulailah dari ruang lingkup kecil, verifikasi di CI producer, dan hindari kontrak yang terlalu kaku. Dengan pendekatan itu, tim kecil pun bisa mendapatkan manfaat besar dari contract test API tanpa harus langsung bergantung pada tooling yang berat.