Contract test API berguna ketika dua komponen saling terhubung tetapi dirilis secara terpisah: misalnya frontend dengan backend, service A dengan service B, atau gateway dengan downstream service. Tujuannya bukan menguji seluruh sistem, melainkan memastikan ekspektasi consumer terhadap request dan response tetap dipenuhi oleh provider.

Kalau tim Anda sering mengalami bug seperti field hilang, nama field berubah, enum bertambah tanpa penanganan, atau status code berubah diam-diam, contract test memberi sinyal cepat sebelum merge atau deploy. Dengan pendekatan ini, provider bisa memverifikasi apakah perubahan API masih kompatibel terhadap consumer yang sudah ada, tanpa harus menyalakan seluruh environment end-to-end.

Apa itu contract test API?

Contract test adalah pengujian yang memverifikasi kontrak interaksi antara dua pihak:

  • Consumer: pihak yang memakai API dan memiliki ekspektasi tertentu.
  • Provider: pihak yang menyediakan API dan harus memenuhi ekspektasi tersebut.

Kontrak biasanya mencakup hal-hal berikut:

  • Endpoint dan method HTTP
  • Header penting
  • Bentuk request
  • Status code yang diharapkan
  • Struktur response
  • Tipe data field
  • Nilai enum atau pola data tertentu

Yang diuji bukan implementasi internal provider, melainkan apakah provider masih mengembalikan perilaku yang disepakati consumer.

Intinya: unit test menjawab “apakah fungsi ini benar?”, integration test menjawab “apakah komponen ini bisa terhubung?”, sedangkan contract test menjawab “apakah integrasi antar pihak masih sesuai kesepakatan?”.

Kapan contract test lebih tepat dibanding jenis test lain?

1. Dibanding unit test

Unit test fokus pada logika internal: parsing, validasi, mapper, formatter, dan sebagainya. Unit test cepat dan penting, tetapi tidak cukup untuk menangkap regression di batas antar service.

Contoh: backend mengubah customer_name menjadi full_name. Unit test backend bisa tetap hijau karena mapper internal sudah diperbarui. Namun frontend yang masih membaca customer_name akan rusak. Di sini contract test lebih tepat karena ia menguji output yang dikonsumsi pihak lain.

2. Dibanding integration test

Integration test memverifikasi beberapa komponen benar-benar terhubung, misalnya service dengan database, service dengan cache, atau API client dengan service sungguhan. Test ini berguna, tetapi cenderung lebih lambat, lebih rapuh terhadap environment, dan sering sulit dipelihara lintas tim.

Contract test lebih efisien untuk kasus berikut:

  • Consumer dan provider dikerjakan tim berbeda
  • Jadwal rilis tidak sinkron
  • Sulit menyiapkan environment integrasi lengkap
  • Butuh validasi kompatibilitas sebelum merge

3. Dibanding end-to-end test

End-to-end test tetap berguna untuk alur bisnis utama, tetapi biasanya mahal, lambat, dan memberi sinyal yang kurang spesifik. Saat E2E gagal, Anda masih harus mencari apakah masalah ada di UI, API gateway, provider, auth, data test, atau network.

Contract test lebih cepat memberi jawaban: apakah perubahan API ini memutus consumer yang diketahui?

Ringkasnya

  • Unit test: validasi logika kecil secara lokal.
  • Integration test: validasi koneksi antar komponen nyata.
  • Contract test: validasi kompatibilitas antarmuka antar service.
  • End-to-end test: validasi alur bisnis dari sudut pandang pengguna.

Keempatnya bukan saling menggantikan. Contract test mengisi celah yang sering tidak tertutup oleh tiga jenis test lainnya.

Model dasar: consumer expectation dan provider contract

Consumer menulis ekspektasi

Consumer mendefinisikan apa yang benar-benar dibutuhkan, bukan seluruh kemungkinan respons provider. Ini penting agar kontrak tidak terlalu ketat.

Misalnya frontend hanya butuh:

  • status code 200
  • field id, name, status
  • status bernilai salah satu dari enum yang dikenali

Consumer tidak seharusnya memaksa field tambahan yang tidak dipakai, urutan properti JSON, atau detail implementasi yang tidak relevan.

Provider memverifikasi kontrak

Provider menjalankan verifikasi terhadap kontrak yang dibuat consumer. Jika implementasi provider tidak lagi memenuhi kontrak, verifikasi gagal. Inilah mekanisme utama pencegahan regression.

Alur sederhananya:

  1. Consumer menulis contract test berdasarkan kebutuhan nyata.
  2. Contract dihasilkan sebagai artefak yang bisa dibagikan.
  3. Provider menjalankan verifikasi terhadap artefak tersebut.
  4. CI memblok merge atau deploy jika verifikasi gagal.

Contoh struktur contract test yang praktis

Contoh berikut bersifat generik agar tidak terikat pada tool tertentu. Bayangkan ada consumer bernama web-frontend yang memanggil endpoint GET /orders/{id}.

Contoh kontrak dari sisi consumer

{
  "consumer": "web-frontend",
  "provider": "order-service",
  "interaction": {
    "description": "mengambil detail order untuk ditampilkan di halaman ringkasan",
    "request": {
      "method": "GET",
      "path": "/orders/ORD-123"
    },
    "expected_response": {
      "status": 200,
      "headers": {
        "content-type": "application/json"
      },
      "body": {
        "id": "ORD-123",
        "status": "PAID",
        "total_amount": 150000,
        "currency": "IDR",
        "customer": {
          "id": "CUS-9",
          "name": "Budi"
        }
      }
    }
  }
}

Dari contoh itu, consumer menyatakan bahwa untuk skenario tertentu, ia membutuhkan struktur respons tersebut. Provider bebas memiliki field tambahan, selama field yang dibutuhkan consumer tetap tersedia dan kompatibel.

Contoh verifikasi di sisi provider

Di provider, verifikasi biasanya membaca kontrak lalu menjalankan endpoint terhadap state data yang sudah disiapkan.

// Pseudocode verifikasi provider
setupProviderState("order ORD-123 exists and is paid")
response = httpGet("/orders/ORD-123")
assert response.status == 200
assert response.body.id == "ORD-123"
assert response.body.status in ["PAID", "PENDING", "CANCELLED"]
assert type(response.body.total_amount) == number
assert type(response.body.customer.name) == string

Poin pentingnya adalah provider state: provider perlu menyiapkan kondisi data yang sesuai agar verifikasi deterministik. Tanpa state yang jelas, test bisa flaky karena bergantung pada database atau data lingkungan yang berubah-ubah.

Skenario perubahan yang sering memicu regression

1. Mengganti nama field

Perubahan berikut tampak sepele tetapi sering memutus consumer:

// Sebelumnya
{
  "customer_name": "Budi"
}

// Setelah refactor
{
  "full_name": "Budi"
}

Jika consumer masih membaca customer_name, UI bisa kosong atau error parsing. Contract test akan gagal karena field yang diharapkan hilang.

Lebih aman: tambahkan field baru, pertahankan field lama untuk masa transisi, lalu deprecate dengan jelas.

2. Mengubah tipe data field

// Sebelumnya
{
  "total_amount": 150000
}

// Setelah perubahan
{
  "total_amount": "150000"
}

Bagi backend tertentu ini mungkin tidak masalah, tetapi consumer yang mengandalkan tipe numerik bisa rusak. Contract test harus memeriksa tipe data penting, bukan hanya keberadaan field.

3. Menambah nilai enum

Misalnya semula status hanya:

PENDING | PAID | CANCELLED

Lalu provider menambah EXPIRED. Secara teknis ini tampak backward compatible dari sisi schema, tetapi belum tentu aman bagi consumer. Frontend yang menggunakan switch tanpa default bisa menampilkan label kosong.

Karena itu, penambahan enum perlu diperlakukan hati-hati. Contract test bisa membantu dengan dua pendekatan:

  • Consumer memvalidasi hanya nilai yang memang didukung.
  • Consumer menambah test bahwa nilai tak dikenal ditangani secara aman, misalnya fallback ke UNKNOWN.

4. Mengubah status code

// Sebelumnya
HTTP 200
{
  "status": "not_found"
}

// Setelah perubahan
HTTP 404
{
  "message": "Order not found"
}

Perubahan status code bisa benar secara desain API, tetapi tetap menyebabkan regression bila consumer belum siap. Contract test harus memasukkan skenario status code penting: sukses, validasi gagal, data tidak ditemukan, dan unauthorized bila relevan.

5. Mengubah struktur nested object

// Sebelumnya
{
  "customer": {
    "id": "CUS-9",
    "name": "Budi"
  }
}

// Setelah perubahan
{
  "customer_id": "CUS-9",
  "customer_name": "Budi"
}

Perubahan bentuk struktur sering lolos dari unit test provider, tetapi bisa memutus mapper consumer. Contract test menangkap hal ini secara langsung.

Backward compatibility dan versioning kontrak

Apa yang dianggap backward compatible?

Secara umum, perubahan berikut sering aman, tetapi tetap perlu dilihat dari kebutuhan consumer:

  • Menambah field baru yang opsional
  • Menambah endpoint baru
  • Menambah header yang tidak wajib dibaca consumer

Sedangkan perubahan berikut sering breaking:

  • Menghapus field yang sudah dipakai consumer
  • Mengganti nama field
  • Mengubah tipe data field
  • Mengubah arti field yang sudah ada
  • Mengubah status code tanpa koordinasi
  • Mengubah format error response yang digunakan consumer

Versioning kontrak bukan hanya versioning endpoint

Banyak tim langsung berpikir tentang /v1 dan /v2. Padahal dalam praktik, versioning kontrak lebih luas daripada menambah versi endpoint. Yang perlu dilacak adalah:

  • Kontrak mana yang aktif dipakai consumer
  • Perubahan mana yang masih kompatibel
  • Consumer mana yang belum siap terhadap perubahan baru

Dalam workflow yang sehat, setiap perubahan kontrak memiliki riwayat dan bisa diverifikasi terhadap provider sebelum dirilis.

Strategi yang lebih aman

  1. Additive first: utamakan menambah, bukan mengganti atau menghapus.
  2. Deprecate sebelum remove: beri masa transisi untuk field atau endpoint lama.
  3. Komunikasikan perubahan enum dan error format: ini sering dianggap kecil padahal berdampak besar.
  4. Verifikasi kontrak consumer yang masih aktif: jangan hanya cek terhadap implementasi terbaru Anda sendiri.

Workflow verifikasi di CI sebelum merge atau deploy

Nilai terbesar contract test muncul ketika ia masuk ke pipeline, bukan hanya dijalankan manual di laptop.

Workflow yang direkomendasikan

  1. Consumer PR
    Saat consumer berubah, jalankan contract test dari sisi consumer untuk menghasilkan artefak kontrak baru.
  2. Publikasikan artefak kontrak
    Simpan sebagai artefak build atau di repositori yang bisa diakses provider pipeline.
  3. Provider verification
    Saat provider berubah, pipeline mengambil kontrak consumer yang relevan lalu menjalankan verifikasi.
  4. Block merge bila gagal
    Jika provider tidak kompatibel dengan kontrak aktif, merge atau deploy ditolak.
  5. Optional: pre-deploy gate
    Sebelum deploy ke staging/production, jalankan verifikasi lagi terhadap build final.

Contoh alur sederhana di CI

# Pipeline consumer
steps:
  - run unit tests
  - run consumer contract tests
  - publish contract artifact

# Pipeline provider
steps:
  - run unit tests
  - fetch active consumer contracts
  - run provider verification
  - fail build if any contract is broken

Kalau tim Anda kecil, jangan mulai dari orkestrasi kompleks. Cukup pastikan ada dua hal:

  • kontrak dihasilkan dari sisi consumer,
  • provider wajib memverifikasinya sebelum merge atau deploy.

Kapan verifikasi dijalankan?

Paling praktis di dua titik:

  • Sebelum merge: menangkap regression sedini mungkin.
  • Sebelum deploy: memastikan artefak final masih kompatibel.

Jika sumber daya terbatas, prioritaskan verifikasi sebelum merge pada provider karena di situlah breaking change paling sering muncul.

Prinsip desain contract test agar tidak rapuh

1. Uji yang dipakai consumer, bukan semua field

Kontrak yang terlalu detail membuat provider sulit berevolusi. Fokus pada field, status code, dan bentuk data yang benar-benar dipakai consumer.

2. Pisahkan skenario utama dan skenario error

Minimal buat kontrak untuk:

  • respons sukses
  • validasi gagal
  • resource tidak ditemukan
  • unauthorized atau forbidden bila relevan

Regression sering muncul di jalur error response, bukan hanya jalur sukses.

3. Gunakan provider state yang eksplisit

Hindari test yang mengandalkan data acak di environment bersama. Nyatakan state dengan jelas, misalnya order exists and is paid atau customer not found.

4. Jangan samakan contract test dengan schema validation penuh

Schema validation berguna, tetapi contract test lebih fokus pada interaksi consumer-provider yang nyata. Schema yang valid belum tentu aman untuk semua consumer.

5. Pastikan failure message mudah ditindaklanjuti

Jika test gagal, tim harus cepat tahu:

  • kontrak milik consumer mana yang rusak,
  • endpoint mana yang terdampak,
  • field atau status code apa yang berubah.

Sinyal cepat tidak hanya soal durasi test, tetapi juga kejelasan diagnosis.

Common mistakes saat adopsi contract test

Mengunci response terlalu ketat

Contoh: memaksa urutan properti JSON, semua field internal harus ada, atau nilai dinamis harus persis sama. Akibatnya test menjadi noisy dan menghambat perubahan yang sebenarnya aman.

Tidak menguji skenario error

Banyak integrasi rusak justru saat validasi gagal atau resource tidak ditemukan. Jika hanya menguji 200 OK, coverage kontrak Anda belum cukup.

Kontrak ditulis oleh provider, bukan consumer

Jika provider yang mendefinisikan semuanya, hasilnya sering berubah menjadi dokumentasi implementasi, bukan kebutuhan nyata consumer. Consumer harus tetap menjadi sumber ekspektasi.

Tidak ada kebijakan kompatibilitas

Tanpa aturan sederhana tentang perubahan aman dan breaking change, tim akan bingung apakah field boleh dihapus, enum boleh ditambah, atau status code boleh diganti.

Tidak menghubungkan ke CI

Contract test yang hanya dijalankan sesekali tidak banyak membantu mencegah regression. Nilainya muncul saat menjadi gerbang otomatis sebelum perubahan masuk.

Checklist adopsi bertahap untuk tim kecil

Anda tidak perlu mengadopsi semuanya sekaligus. Mulailah dari integrasi yang paling sering rusak.

Tahap 1: pilih satu integrasi kritis

  • Pilih satu consumer-provider yang paling sering menimbulkan bug.
  • Batasi dulu pada 1-3 endpoint penting.
  • Masukkan jalur sukses dan satu jalur error.

Tahap 2: definisikan aturan kompatibilitas minimum

  • Field lama tidak boleh dihapus tanpa masa transisi.
  • Tipe data tidak boleh berubah diam-diam.
  • Status code utama harus stabil.
  • Penambahan enum harus dikomunikasikan dan ditangani consumer.

Tahap 3: masukkan ke CI provider

  • Provider wajib memverifikasi kontrak aktif sebelum merge.
  • Build gagal jika ada kontrak consumer yang rusak.

Tahap 4: perluas secara bertahap

  • Tambahkan endpoint lain yang sering berubah.
  • Tambahkan skenario error yang penting.
  • Tambahkan consumer lain jika provider dipakai banyak pihak.

Tahap 5: rapikan ownership

  • Tentukan siapa pemilik kontrak dari sisi consumer.
  • Tentukan siapa yang menyetujui breaking change.
  • Tentukan masa deprecation dan proses penghapusan field lama.

Tips debugging saat verifikasi kontrak gagal

  • Bandingkan payload aktual vs payload yang diharapkan: fokus ke field yang benar-benar dipakai consumer.
  • Cek provider state: banyak kegagalan berasal dari setup data yang tidak sesuai, bukan perubahan API.
  • Periksa status code lebih dulu: jika status code sudah berbeda, body sering ikut berubah.
  • Cek perubahan serializer/mapper: refactor di layer ini sering memicu field hilang atau rename tak sengaja.
  • Audit enum dan fallback consumer: penambahan nilai baru sering lolos dari review API.
  • Pastikan environment test deterministik: hindari data bersama yang bisa berubah oleh pipeline lain.

Kesimpulan

Contract test API untuk cegah regression antar service paling efektif ketika Anda ingin menjaga kompatibilitas integrasi tanpa bergantung penuh pada test end-to-end yang lambat dan rapuh. Ia sangat cocok untuk relasi frontend-backend maupun service-to-service yang dirilis terpisah.

Praktiknya sederhana: consumer mendefinisikan ekspektasi yang nyata, provider memverifikasi kontrak itu secara otomatis, lalu CI memblok perubahan yang memutus integrasi. Mulailah dari endpoint yang paling kritis, tetapkan aturan backward compatibility yang jelas, dan gunakan contract test sebagai sinyal cepat untuk menurunkan bug integrasi sebelum sampai ke staging atau production.