Contract test membantu memastikan integrasi API tidak rusak diam-diam ketika salah satu service mengubah payload, status code, header, atau perilaku endpoint. Di proyek CodeIgniter 4, pendekatan ini berguna baik untuk komunikasi antar service internal maupun integrasi dengan API pihak ketiga.
Masalah yang sering terjadi bukan endpoint benar-benar mati, melainkan kontrak-nya berubah: field hilang, tipe data berubah, status code bergeser dari 200 ke 201, atau header seperti Content-Type dan Authorization tidak lagi konsisten. Contract test menutup celah ini dengan memverifikasi kesepakatan provider-consumer secara eksplisit sebelum perubahan dirilis.
Apa itu contract test dan kapan dibutuhkan?
Contract test berada di antara unit test dan integration test. Tujuannya bukan menguji seluruh alur end-to-end, melainkan memverifikasi bahwa kontrak komunikasi API tetap kompatibel.
Perbedaan unit test, integration test, dan contract test
- Unit test: menguji fungsi, class, atau method secara terisolasi. Cocok untuk validasi mapper, formatter, validator, atau transformer payload.
- Integration test: menguji interaksi nyata antar komponen, misalnya controller dengan database, atau HTTP client dengan sandbox API.
- Contract test: menguji bahwa request dan response yang dipertukarkan antar pihak tetap sesuai kesepakatan, tanpa harus selalu menjalankan seluruh skenario end-to-end.
Gunakan unit test jika Anda ingin memastikan logika internal benar. Gunakan integration test jika perlu membuktikan komponen benar-benar terhubung. Gunakan contract test jika risiko utama Anda adalah perubahan format API yang mematahkan consumer lain.
Ringkasnya: unit test menjawab “apakah logikanya benar?”, integration test menjawab “apakah komponen-komponen benar-benar terhubung?”, dan contract test menjawab “apakah antarmuka antar service masih kompatibel?”
Yang perlu diverifikasi dalam contract test API
Di CodeIgniter 4, contract test untuk API sebaiknya tidak berhenti di status code. Verifikasi minimal yang berguna biasanya mencakup:
- HTTP method dan path: misalnya
GET /api/orders/{id}. - Header request penting:
Authorization,Accept,Content-Type, idempotency header, atau correlation ID jika dipakai. - Struktur payload request: field wajib, field opsional, tipe data, format tanggal, enum, dan batas validasi dasar.
- Status code response: misalnya 200 untuk sukses, 201 untuk create, 404 untuk resource tidak ditemukan, 422 untuk validasi gagal.
- Header response penting: terutama
Content-Type: application/json, caching header, atau header versi API jika ada. - Skema payload response: field yang harus ada, tipe data, nullability, nested object, dan array item.
- Kompatibilitas perubahan: penambahan field biasanya aman untuk consumer yang toleran, tetapi penghapusan field, penggantian nama field, atau perubahan tipe data biasanya mematahkan kontrak.
Fokus contract test adalah kompatibilitas. Artinya, jangan mengikat test pada detail yang tidak penting, misalnya urutan field JSON, nilai timestamp yang berubah setiap saat, atau pesan error lengkap yang memang tidak dijanjikan sebagai bagian kontrak publik.
Strategi provider-consumer sederhana di CodeIgniter 4
Skema yang umum adalah:
- Consumer mendefinisikan ekspektasi terhadap endpoint provider.
- Provider menjalankan test/verifikasi untuk memastikan response aktual masih memenuhi ekspektasi itu.
- Jika provider berubah dan mematahkan kontrak, pipeline gagal sebelum rilis.
Pada tim kecil, Anda tidak harus langsung memakai broker kontrak atau tooling yang kompleks. Anda bisa memulai dari contract test berbasis fixture JSON dan assertion skema yang disimpan di repository.
Contoh kontrak endpoint
Misalkan ada provider Order Service dengan endpoint:
GET /api/orders/ORD-1001Consumer mengharapkan response seperti ini:
{
"id": "ORD-1001",
"status": "paid",
"total": 150000,
"currency": "IDR",
"customer": {
"id": "CUST-9",
"name": "Budi"
}
}Kontrak pentingnya mungkin:
- status code harus
200 Content-Typeharus JSON- field
id,status,total,currency, dancustomerwajib ada totalharus numerikcustomer.iddancustomer.namewajib ada
Jika provider mengganti total menjadi string atau mengubah customer.name menjadi full_name tanpa kompatibilitas, contract test harus gagal.
Struktur test di CodeIgniter 4
CodeIgniter 4 mendukung pengujian berbasis PHPUnit. Untuk contract test, pisahkan direktori test agar maksudnya jelas dan tidak bercampur dengan unit test biasa.
tests/
├── unit/
├── integration/
└── contract/
├── provider/
│ └── OrdersContractTest.php
├── consumer/
│ └── OrdersApiConsumerContractTest.php
└── fixtures/
├── orders_show_success.json
└── orders_show_not_found.jsonPemisahan ini membantu tim memahami jenis kegagalan. Jika test gagal di unit, biasanya masalah logika internal. Jika gagal di contract, masalah ada pada kesepakatan integrasi.
Contoh fixture yang stabil
Gunakan fixture yang sengaja dibuat stabil, kecil, dan representatif. Hindari field yang nilainya berubah-ubah kecuali memang bagian dari kontrak.
{
"id": "ORD-1001",
"status": "paid",
"total": 150000,
"currency": "IDR",
"customer": {
"id": "CUST-9",
"name": "Budi"
}
}Simpan sebagai tests/contract/fixtures/orders_show_success.json.
Contoh provider contract test
Berikut contoh sederhana untuk memverifikasi response endpoint di sisi provider. Implementasi dapat disesuaikan dengan base test case proyek Anda.
<?php
namespace Tests\Contract\Provider;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;
class OrdersContractTest extends CIUnitTestCase
{
use FeatureTestTrait;
private function loadFixture(string $file): array
{
$path = TESTPATH . 'contract/fixtures/' . $file;
return json_decode(file_get_contents($path), true);
}
public function testGetOrderMatchesConsumerContract(): void
{
$expected = $this->loadFixture('orders_show_success.json');
$result = $this->get('/api/orders/ORD-1001', [
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer test-token'
]
]);
$result->assertStatus(200);
$result->assertHeader('Content-Type');
$json = json_decode($result->getJSON(), true);
$this->assertIsArray($json);
$this->assertSame($expected['id'], $json['id'] ?? null);
$this->assertSame($expected['status'], $json['status'] ?? null);
$this->assertIsNumeric($json['total'] ?? null);
$this->assertSame($expected['currency'], $json['currency'] ?? null);
$this->assertArrayHasKey('customer', $json);
$this->assertSame($expected['customer']['id'], $json['customer']['id'] ?? null);
$this->assertArrayHasKey('name', $json['customer']);
}
}Poin penting dari contoh di atas:
- Test tidak membandingkan seluruh JSON mentah secara kaku jika tidak perlu.
- Field wajib diverifikasi satu per satu.
- Tipe data dan keberadaan field diperiksa eksplisit.
- Fixture dipakai sebagai referensi kontrak yang mudah ditinjau saat code review.
Contoh consumer-side contract test
Di sisi consumer, test fokus pada parser atau adapter yang mengonsumsi API. Tujuannya memastikan consumer benar-benar hanya bergantung pada kontrak yang disepakati.
<?php
namespace Tests\Contract\Consumer;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;
class OrdersApiConsumerContractTest extends CIUnitTestCase
{
public function testConsumerCanParseContractPayload(): void
{
$payload = json_decode(file_get_contents(
TESTPATH . 'contract/fixtures/orders_show_success.json'
), true);
$this->assertSame('ORD-1001', $payload['id']);
$this->assertContains($payload['status'], ['paid', 'pending', 'cancelled']);
$this->assertIsNumeric($payload['total']);
$this->assertSame('IDR', $payload['currency']);
$this->assertArrayHasKey('customer', $payload);
}
}Walaupun sederhana, pola ini membantu mencegah consumer menambahkan asumsi liar, misalnya menganggap semua response selalu memiliki field yang sebenarnya opsional.
Verifikasi skema payload tanpa membuat test rapuh
Kesalahan umum dalam contract test adalah menyamakan contract test dengan snapshot test total. Snapshot penuh memang cepat dibuat, tetapi sering rapuh. Sedikit perubahan non-esensial bisa memicu kegagalan yang tidak relevan.
Apa yang sebaiknya dikunci
- nama field wajib
- tipe data utama
- status code
- header penting
- format field yang memang dipublikasikan, misalnya ISO-8601 untuk tanggal
Apa yang sebaiknya lebih longgar
- urutan field JSON
- field tambahan yang kompatibel ke belakang
- timestamp yang memang dinamis
- message error detail jika tidak menjadi bagian kontrak publik
Jika tim Anda memakai JSON Schema atau validator sejenis, itu bisa membantu. Namun jika belum, assertion eksplisit seperti pada contoh sebelumnya sudah cukup baik untuk tahap awal, selama fokusnya pada kompatibilitas kontrak.
Perubahan kompatibel vs perubahan yang mematahkan kontrak
Biasanya kompatibel
- menambahkan field baru yang opsional
- menambahkan endpoint baru
- menambahkan header response tambahan yang tidak mengubah perilaku lama
Biasanya mematahkan kontrak
- menghapus field yang dipakai consumer
- mengubah tipe data, misalnya angka menjadi string
- mengganti nama field tanpa masa transisi
- mengubah status code untuk skenario yang sama
- mengubah format tanggal atau enum tanpa koordinasi
- mengubah makna field yang sama
Praktik aman adalah menerapkan perubahan breaking melalui versi baru endpoint atau masa transisi yang jelas. Contract test berguna karena ia memaksa tim menyadari bahwa “ubah kecil” di provider bisa menjadi breaking change bagi consumer.
Skenario provider-consumer sederhana yang realistis
Anggap ada dua aplikasi:
- Billing Service sebagai provider, menyediakan endpoint order.
- Portal Admin sebagai consumer, menampilkan data order ke staf operasional.
Portal Admin membutuhkan id, status, total, dan customer.name. Suatu hari developer Billing Service mengganti customer.name menjadi customer.full_name agar lebih deskriptif. Unit test provider bisa saja tetap hijau, integration test ke database juga tetap hijau, tetapi halaman Portal Admin akan rusak saat deploy.
Dengan contract test, perubahan ini tertangkap lebih awal karena kontrak consumer tidak lagi terpenuhi.
Menjalankan contract test di CI agar regresi terdeteksi sebelum rilis
Contract test sebaiknya menjadi bagian pipeline build. Minimal, jalankan test ini pada:
- pull request yang mengubah controller, response formatter, serializer, DTO, atau client API
- merge ke branch utama
- rilis kandidat
Contoh perintah umum
php vendor/bin/phpunitJika suite dipisahkan, Anda bisa mengarahkan eksekusi ke direktori contract:
php vendor/bin/phpunit tests/contractDetail nama binary atau konfigurasi dapat berbeda antar proyek, tetapi prinsipnya sama: contract test harus otomatis berjalan, bukan manual.
Strategi di pipeline CI
- Siapkan environment test yang konsisten.
- Load fixture atau seed data yang deterministik.
- Jalankan unit test lebih dulu karena paling cepat.
- Jalankan contract test setelah build aplikasi siap.
- Gagalkan pipeline jika ada perubahan kontrak yang tidak kompatibel.
Untuk provider yang bergantung pada database, gunakan data seed yang stabil. Jangan biarkan contract test membaca data acak dari environment bersama karena hasilnya sulit diprediksi.
Fixture dan data uji yang stabil
Contract test yang baik sangat bergantung pada fixture yang stabil. Banyak kegagalan palsu muncul karena data test tidak konsisten, bukan karena kontrak benar-benar rusak.
Prinsip fixture yang baik
- Deterministik: nilai sama untuk setiap eksekusi.
- Kecil: hanya memuat field yang relevan.
- Representatif: mencerminkan payload nyata, bukan payload mainan yang terlalu sederhana.
- Terversi: perubahan fixture ditinjau lewat code review.
Hindari hal berikut
- ID acak yang berubah setiap run
- timestamp real-time tanpa pembekuan waktu
- dependensi ke layanan eksternal saat menjalankan contract test lokal
- payload hasil copy-paste dari production yang terlalu besar dan berisik
Penyebab false positive dan false negative
False positive
False positive terjadi ketika test gagal padahal tidak ada regresi kontrak yang bermakna.
- Test membandingkan seluruh JSON mentah termasuk field tambahan yang sebenarnya aman.
- Urutan field dianggap penting padahal tidak relevan.
- Nilai dinamis seperti timestamp atau trace ID ikut divalidasi keras.
- Fixture tidak sinkron dengan kontrak yang benar-benar disepakati.
Solusinya adalah memperketat hanya pada bagian kontrak yang penting dan membuat matcher yang lebih toleran terhadap data non-esensial.
False negative
False negative terjadi ketika test lolos padahal ada kerusakan kontrak nyata.
- Test hanya memeriksa status code 200 tanpa memeriksa payload.
- Header penting seperti
Content-TypeatauAuthorizationtidak diuji. - Test memakai mock yang terlalu jauh dari perilaku nyata provider.
- Consumer diam-diam bergantung pada field yang tidak masuk kontrak, tetapi test tidak merekam kebutuhan itu.
Solusinya adalah meninjau kontrak dari sudut pandang consumer yang benar-benar memakai endpoint tersebut, bukan hanya dari asumsi provider.
Debugging saat contract test gagal
- Bandingkan payload aktual dan fixture/kontrak, lalu cari apakah perubahannya kompatibel atau breaking.
- Periksa apakah kegagalan berasal dari data seed yang berubah, bukan dari kode endpoint.
- Pastikan header response benar-benar terkirim seperti yang diharapkan.
- Lihat perubahan pada serializer, transformer, resource formatter, atau model mapping.
- Jika integrasi pihak ketiga berubah, buat fixture baru dari dokumentasi atau sandbox terbaru lalu putuskan apakah adapter consumer perlu diperbarui.
Jangan langsung memperbarui fixture hanya agar test hijau. Pertama, jawab dulu: apakah perubahan ini memang sengaja dan kompatibel? Jika tidak, memperbarui fixture hanya menyembunyikan regresi.
Kapan perlu integration test juga?
Contract test bukan pengganti semua test integrasi. Anda tetap memerlukan integration test jika:
- ingin membuktikan autentikasi dan otorisasi berjalan terhadap komponen nyata
- ingin memverifikasi query database, transaction, atau side effect tertentu
- endpoint bergantung pada middleware, filter, cache, atau konfigurasi runtime yang signifikan
- perlu membuktikan integrasi ke sandbox API pihak ketiga memang bisa diakses
Strategi praktisnya:
- Unit test untuk logika internal dan transformasi data
- Contract test untuk menjaga bentuk antarmuka API
- Integration test untuk membuktikan wiring sistem dan dependency nyata
Checklist adopsi bertahap untuk tim kecil
Tim kecil tidak perlu memulai dengan semua endpoint sekaligus. Mulai dari endpoint yang paling berisiko.
- Identifikasi 3-5 endpoint yang paling sering dipakai consumer lain.
- Tentukan field wajib, status code, dan header penting untuk tiap endpoint itu.
- Buat fixture JSON stabil untuk skenario sukses dan satu-dua skenario gagal penting.
- Susun folder
tests/contractterpisah dari unit/integration test. - Tulis assertion eksplisit untuk field dan tipe data utama.
- Tambahkan eksekusi contract test ke CI pada pull request.
- Sepakati aturan perubahan breaking: versi baru, masa transisi, atau deprecation notice.
- Evaluasi endpoint lain secara bertahap setelah pola kerja tim stabil.
Pendekatan bertahap lebih realistis daripada mencoba mengontrak seluruh API sekaligus, yang sering berakhir dengan test berisik dan tidak dipelihara.
Kesimpulan
CodeIgniter 4 contract test efektif untuk mencegah regresi integrasi API yang tidak selalu tertangkap oleh unit test atau integration test biasa. Nilai utamanya ada pada verifikasi kontrak request/response: status code, header penting, struktur payload, tipe data, dan kompatibilitas perubahan.
Untuk implementasi awal, Anda tidak wajib memakai tooling kompleks. Struktur test yang rapi, fixture stabil, assertion yang fokus pada kontrak penting, dan eksekusi otomatis di CI sudah cukup untuk memberi perlindungan nyata. Setelah itu, tim bisa memperluas cakupan test secara bertahap sesuai risiko integrasi yang ada.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!