Contract test API membantu tim mendeteksi regresi integrasi antarservis lebih cepat dibanding menunggu end-to-end test gagal di lingkungan staging. Untuk backend Rust yang menyediakan atau mengonsumsi HTTP JSON, pendekatan ini berguna saat perubahan kecil seperti field baru, status code berbeda, atau format error yang bergeser dapat memutus integrasi meskipun unit test tetap hijau.
Intinya, contract test memverifikasi kesepakatan antara provider dan consumer API: bentuk request, struktur response, status code, header penting, dan perilaku error. Dengan begitu, perubahan antarmuka bisa ditangkap di CI sebelum dirilis, tanpa biaya dan ketidakstabilan yang biasanya muncul pada pengujian end-to-end penuh.
Kapan contract test lebih efektif daripada hanya unit test atau end-to-end test
Keterbatasan unit test
Unit test kuat untuk memeriksa logika lokal: validasi input, mapping struct, transformasi data, atau perilaku handler dalam isolasi. Namun, unit test biasanya tidak cukup untuk memastikan hal-hal berikut:
- Nama field JSON tetap sama.
- Field yang wajib tetap ada.
- Status code HTTP tidak berubah tanpa sengaja.
- Bentuk error response masih bisa dipahami consumer.
- Provider dan consumer memiliki asumsi yang sama tentang tipe data.
Contoh umum: provider mengganti user_id menjadi id, atau mengubah status code dari 404 menjadi 200 dengan body kosong. Unit test internal provider bisa tetap lolos, tetapi consumer rusak.
Keterbatasan end-to-end test
End-to-end test penting karena menguji integrasi nyata antarkomponen. Masalahnya, test jenis ini sering:
- Lambat dijalankan.
- Bergantung pada banyak service sekaligus.
- Sulit didiagnosis saat gagal.
- Lebih mudah flaky karena data, jaringan, waktu, atau dependensi eksternal.
Contract test berada di tengah: cakupannya lebih luas daripada unit test, tetapi lebih terfokus dan deterministik daripada end-to-end test.
Kapan contract test paling tepat
Gunakan contract test saat:
- Ada lebih dari satu service yang berkomunikasi via HTTP JSON.
- Tim provider dan consumer berkembang terpisah.
- Perubahan API sering terjadi.
- Kegagalan integrasi biasanya baru terdeteksi di staging atau produksi.
- Anda ingin validasi kompatibilitas di CI tanpa menghidupkan seluruh sistem.
Praktiknya: unit test memeriksa logika internal, contract test memeriksa antarmuka integrasi, dan end-to-end test memeriksa alur bisnis penting dari ujung ke ujung. Ketiganya saling melengkapi, bukan saling menggantikan.
Apa yang sebaiknya divalidasi dalam contract test API di Rust
Pada konteks HTTP JSON, kontrak sebaiknya mencakup elemen yang benar-benar diandalkan consumer.
Request yang dikirim consumer
- Method HTTP:
GET,POST, dan seterusnya. - Path dan parameter query.
- Header penting seperti
AuthorizationatauContent-Type. - Body JSON dan field wajib.
Response dari provider
- Status code yang diharapkan.
- Header penting bila memang digunakan consumer.
- Struktur JSON: field wajib, tipe data, nested object, dan array.
- Nilai tertentu yang punya makna kontraktual, misalnya enum status.
Error response
Bagian ini sering terlupakan, padahal regresi integrasi justru banyak terjadi di jalur error. Pastikan Anda memverifikasi:
- Status code untuk skenario gagal, misalnya
400,404, atau409. - Struktur body error, misalnya
code,message, dandetails. - Perbedaan antara error validasi dan error domain.
Jika consumer bergantung pada code tertentu untuk branching logic, field itu harus menjadi bagian eksplisit dari kontrak.
Contoh arsitektur sederhana: provider dan consumer Rust berbasis HTTP JSON
Misalkan ada dua service:
- user-service sebagai provider, menyediakan endpoint
GET /users/{id}. - order-service sebagai consumer, memanggil
user-serviceuntuk mengambil data user sebelum memproses order.
Kontrak yang disepakati:
GET /users/123HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "123",
"name": "Nadia",
"email": "[email protected]",
"status": "active"
}Dan untuk user yang tidak ada:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"code": "USER_NOT_FOUND",
"message": "user not found"
}Di Rust, Anda bisa merepresentasikan bentuk data ini dengan struct yang jelas agar perubahan kontrak lebih terlihat.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct UserResponse {
id: String,
name: String,
email: String,
status: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ApiError {
code: String,
message: String,
}Struct ini sendiri belum cukup menjadi contract test. Nilainya baru muncul ketika provider dan consumer sama-sama memverifikasi bahwa payload nyata sesuai kontrak yang disepakati.
Struktur contract test yang praktis di proyek Rust
Pola consumer-driven contract
Pola yang paling praktis untuk tim kecil biasanya consumer-driven contract. Consumer mendefinisikan ekspektasi minimum terhadap provider. Provider lalu memverifikasi bahwa implementasinya masih memenuhi ekspektasi itu.
Keuntungannya:
- Kontrak berangkat dari kebutuhan nyata consumer.
- Provider tidak perlu menebak field mana yang benar-benar dipakai.
- Perubahan breaking bisa terdeteksi sebelum deployment.
Struktur direktori yang mudah dipelihara
Contoh struktur repository:
project/
user-service/
src/
tests/
contract_provider.rs
contracts/
user_get_by_id_v1.json
order-service/
src/
tests/
contract_consumer.rs
contracts/
user_get_by_id_v1.json
fixtures/
users/
active_user.json
user_not_found.jsonPemisahan ini membantu karena:
- Fixture bisa dipakai ulang.
- Kontrak terdokumentasi dalam bentuk file.
- Perubahan kontrak bisa direview melalui pull request.
Contoh test consumer di Rust
Consumer dapat menjalankan test terhadap mock server agar request dan parsing response tervalidasi secara deterministik. Contoh berikut bersifat generik dan tidak bergantung pada framework tertentu.
#[tokio::test]
async fn get_user_returns_expected_shape() {
// 1. Siapkan mock server yang mengembalikan kontrak yang disepakati.
// 2. Panggil client HTTP dari order-service.
// 3. Verifikasi request yang dikirim dan response yang diparsing.
let body = r#"{
"id": "123",
"name": "Nadia",
"email": "[email protected]",
"status": "active"
}"#;
// pseudo:
// let server = spawn_mock_server("GET", "/users/123", 200, body);
// let client = UserClient::new(server.base_url());
// let user = client.get_user("123").await.unwrap();
let user: UserResponse = serde_json::from_str(body).unwrap();
assert_eq!(user.id, "123");
assert_eq!(user.status, "active");
}Yang penting bukan library mock tertentu, melainkan prinsipnya:
- Consumer memverifikasi request yang dia kirim memang sesuai kontrak.
- Consumer memverifikasi bahwa response provider dalam bentuk yang disepakati bisa diproses tanpa kejutan.
Contoh verifikasi provider di Rust
Di sisi provider, test memanggil handler HTTP lokal atau menjalankan server test, lalu memastikan output aktual cocok dengan kontrak.
#[tokio::test]
async fn provider_returns_404_error_contract() {
// pseudo:
// let app = test_app_with_fixture_data();
// let response = app.get("/users/999").await;
let status = 404;
let body = r#"{
"code": "USER_NOT_FOUND",
"message": "user not found"
}"#;
assert_eq!(status, 404);
let err: ApiError = serde_json::from_str(body).unwrap();
assert_eq!(err.code, "USER_NOT_FOUND");
}Test provider tidak hanya memeriksa bahwa endpoint berjalan, tetapi bahwa responsenya tetap kompatibel dengan kontrak yang telah dipublikasikan.
Fixture data: kecil, stabil, dan eksplisit
Fixture adalah salah satu sumber kejelasan sekaligus sumber kekacauan jika tidak dikelola. Untuk contract test, fixture sebaiknya:
- Kecil dan fokus pada satu skenario.
- Tidak bergantung pada data produksi.
- Tidak berubah karena waktu berjalan.
- Mewakili skenario sukses dan gagal.
Contoh fixture yang baik
{
"id": "123",
"name": "Nadia",
"email": "[email protected]",
"status": "active"
}Dan fixture error:
{
"code": "USER_NOT_FOUND",
"message": "user not found"
}Hindari fixture yang membuat test tidak deterministik
- Timestamp real-time yang berubah di setiap run.
- ID acak yang tidak dikontrol.
- Urutan array yang tidak stabil.
- Ketergantungan pada database bersama.
Jika API memang mengandung field dinamis seperti created_at atau request_id, validasi hanya bagian yang kontraktual. Misalnya, cek bahwa field ada dan bertipe string, bukan memaksa nilainya identik setiap kali.
Sumber flaky test pada integrasi API dan cara menguranginya
Flaky test membuat tim kehilangan kepercayaan pada pipeline. Pada contract testing, flaky biasanya datang bukan dari konsep kontrak itu sendiri, melainkan dari cara pengujiannya disusun.
1. Port dan network dependency
Menjalankan server sungguhan pada port tetap bisa bentrok di CI atau saat test paralel. Gunakan port acak yang dialokasikan runtime, atau panggil handler secara in-process bila memungkinkan.
2. Waktu dan timezone
Field tanggal yang dihasilkan berdasarkan waktu sekarang sering memicu mismatch. Gunakan clock yang bisa diinjeksi agar test memakai waktu tetap.
3. Shared state
Jika beberapa test memakai database atau cache yang sama, hasilnya bisa tergantung urutan eksekusi. Lebih aman memakai data setup per test atau reset state di awal.
4. Ketergantungan ke service eksternal
Contract test sebaiknya tidak memanggil service nyata di jaringan luar. Begitu test bergantung pada DNS, latensi, rate limit, atau sandbox pihak ketiga, ia berubah menjadi test integrasi lingkungan dan rawan gagal acak.
5. JSON comparison yang terlalu kaku
Membandingkan string JSON mentah sering gagal hanya karena urutan field berbeda. Bandingkan sebagai objek terstruktur, lalu validasi field yang penting.
Tips debugging: saat contract test gagal, log-kan request, status code, header penting, dan body response yang sudah dipretty-print. Kegagalan kontrak sering lebih cepat dipahami dari diff payload dibanding dari stack trace parser.
Memvalidasi perubahan field, status code, dan error response
Perubahan field
Perubahan kontrak yang paling umum:
- Rename field, misalnya
user_idmenjadiid. - Menghapus field yang dipakai consumer.
- Mengubah tipe data, misalnya integer menjadi string.
- Menjadikan field nullable tanpa pemberitahuan.
Secara umum:
- Menambah field baru sering aman jika consumer mengabaikan field yang tidak dipakai.
- Menghapus atau mengganti nama field hampir selalu breaking.
- Mengubah tipe data adalah breaking meskipun nama field tetap.
Di Rust, perubahan tipe akan cepat terdeteksi saat deserialisasi gagal, yang justru menjadi keuntungan karena masalah muncul lebih awal.
Perubahan status code
Status code adalah bagian penting dari kontrak. Consumer sering memakai status code untuk flow control. Misalnya:
404berarti user tidak ada.409berarti konflik bisnis.422berarti validasi gagal.
Jika provider mengubah 404 menjadi 200 dengan body kosong, secara teknis endpoint masih merespons, tetapi kontraknya rusak. Contract test harus memeriksa status code sebagai assertion eksplisit, bukan hanya body.
Error response
Banyak tim hanya mengetes jalur sukses, lalu produksi rusak ketika format error berubah. Minimal, sediakan contract test untuk:
- Not found.
- Validation error.
- Unauthorized atau forbidden jika memang relevan.
- Conflict untuk operasi write.
Jika format error Anda seragam, dokumentasikan dan uji bentuk minimumnya. Contoh:
{
"code": "VALIDATION_ERROR",
"message": "email is invalid",
"details": {
"field": "email"
}
}Bila details opsional, nyatakan itu jelas dalam kontrak dan jangan membuat consumer mengasumsikan field tersebut selalu ada.
Versioning kontrak tanpa membuat maintenance meledak
Versioning perlu dilakukan saat ada perubahan yang tidak kompatibel ke belakang. Tujuannya bukan menambah kompleksitas, tetapi memberi jalur migrasi yang aman.
Kapan perlu versi baru
- Menghapus field yang dipakai consumer.
- Mengubah tipe field.
- Mengubah status code untuk skenario yang sama.
- Mengubah makna domain dari field atau nilai enum.
Kapan biasanya tidak perlu versi baru
- Menambah field baru yang bisa diabaikan consumer.
- Menambah endpoint baru tanpa memengaruhi endpoint lama.
- Menambah nilai enum baru, jika consumer memang dirancang menangani unknown value dengan aman. Jika tidak, ini tetap berpotensi breaking.
Pendekatan versioning yang realistis
Untuk tim kecil, jangan mulai dengan skema kompleks. Cukup:
- Simpan file kontrak per endpoint dan versi, misalnya
user_get_by_id_v1.json. - Jika ada breaking change, buat
v2dan jalankan verifikasi untuk v1 dan v2 selama masa transisi. - Tetapkan tanggal atau milestone penghentian versi lama.
Hal yang lebih penting daripada format versioning adalah disiplin menandai mana perubahan breaking dan mana yang kompatibel.
Alur verifikasi contract test API di CI
Agar berguna, contract test harus menjadi bagian dari alur perubahan kode, bukan hanya dijalankan manual.
Alur yang disarankan
- Consumer memperbarui atau menambah kontrak berdasarkan kebutuhan baru.
- Consumer menjalankan test lokal terhadap mock server.
- File kontrak dipublikasikan atau disimpan di lokasi yang bisa diakses provider.
- Provider menjalankan verifikasi terhadap kontrak tersebut di CI.
- Pull request provider gagal jika implementasi tidak lagi memenuhi kontrak aktif.
Pipeline minimum untuk tim kecil
- Job 1: unit test biasa.
- Job 2: consumer contract test dengan mock provider.
- Job 3: provider verification terhadap kontrak yang dipakai consumer.
Dengan model ini, Anda tidak harus menyalakan seluruh lingkungan staging hanya untuk tahu bahwa field status berubah nama.
Apa yang perlu diarsipkan di CI
- File kontrak yang diverifikasi.
- Log request/response saat gagal.
- Diff payload atau assertion yang tidak cocok.
Artefak ini penting agar tim provider dan consumer bisa mendiskusikan perubahan dengan bukti yang jelas, bukan dugaan.
Trade-off biaya maintenance dan batasan contract testing
Contract testing bukan gratis. Ia menambah artefak, review, dan pipeline baru. Karena itu, penting memahami biaya dan manfaatnya.
Biaya yang nyata
- Perlu menjaga file kontrak tetap relevan.
- Perlu fixture dan skenario error yang rapi.
- Perlu koordinasi antartim saat kontrak berubah.
- Perlu disiplin agar kontrak tidak terlalu detail.
Kesalahan umum yang membuat biaya membengkak
- Mengunci terlalu banyak detail yang tidak dipakai consumer.
- Menjadikan semua field wajib padahal sebagian opsional.
- Menggunakan data acak dan state bersama.
- Menganggap contract test bisa menggantikan end-to-end test sepenuhnya.
Batasan contract testing
- Tidak membuktikan seluruh sistem terhubung benar di lingkungan nyata.
- Tidak menggantikan pengujian performa, security, atau resiliency.
- Tidak otomatis menangkap bug logika bisnis internal jika bentuk API tetap sama.
Karena itu, contract test paling efektif jika dipakai untuk menjaga antarmuka tetap stabil, bukan sebagai satu-satunya strategi quality assurance.
Checklist implementasi bertahap untuk tim kecil
Jika tim Anda belum pernah menjalankan contract test API, mulai kecil dan fokus pada endpoint yang paling sering menyebabkan insiden integrasi.
- Pilih 1 consumer dan 1 provider yang kritis.
- Ambil 1 endpoint read-only lebih dulu, misalnya
GET /users/{id}. - Definisikan kontrak minimum: method, path, status code, body sukses, dan body error utama.
- Buat fixture statis untuk skenario sukses dan not found.
- Tambahkan test consumer terhadap mock server.
- Tambahkan test provider yang memverifikasi implementasi lokal terhadap kontrak.
- Masukkan ke CI sebagai job terpisah agar mudah dibaca saat gagal.
- Review perubahan kontrak di pull request seperti halnya perubahan schema database.
- Tambahkan endpoint write setelah pola stabil, termasuk validasi error
400atau409. - Tetapkan aturan breaking change dan kapan versi baru harus dibuat.
Penutup
Untuk backend Rust yang saling terhubung lewat HTTP JSON, contract test adalah cara praktis untuk mencegah regresi integrasi sebelum perubahan mencapai staging atau produksi. Nilai utamanya bukan pada jumlah test, melainkan pada ketepatan memverifikasi hal-hal yang benar-benar dipakai antarservis: field penting, tipe data, status code, dan error response.
Mulailah dari satu integrasi yang sering bermasalah, buat fixture yang deterministik, dan pasang verifikasi di CI. Dengan pendekatan itu, Anda mendapat sinyal yang lebih cepat dan lebih stabil dibanding hanya mengandalkan unit test atau end-to-end test penuh.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!