Kodeloka
  1. Kodeloka
  2. Kelas Microservices
  3. Materi

Keandalan · Modul 19 dari 24

Contract Testing & Strategi Pengujian Microservices

Menguji integrasi antar layanan tanpa menjalankan seluruh sistem: piramida uji, consumer-driven contract, dan Testcontainers.

1. Kenapa Piramida Uji Berubah di Microservices

          ▲  E2E  (sedikit, lambat, rapuh)
         ╱ ╲
        ╱   ╲  Contract  ← lapisan yang KHAS microservices
       ╱     ╲
      ╱       ╲  Integration (dengan dependensi nyata di kontainer)
     ╱         ╲
    ╱___________╲  Unit (banyak, cepat, murah)
🚫 Perangkap end-to-end test

Menjalankan 15 layanan plus database plus broker untuk setiap pull request terdengar menyeluruh, tetapi hasilnya: pipeline berjalan 40 menit, uji gagal secara acak karena masalah waktu dan jaringan, tim mulai mengabaikan kegagalan ("ah, itu cuma flaky"), dan pada akhirnya uji tersebut tidak lagi memberi keyakinan apa pun. Sedikit E2E untuk alur bisnis paling kritis saja — sisanya diselesaikan di lapisan yang lebih rendah.

2. Masalah Mock yang Menipu

// Uji di Layanan Pesanan, memakai mock Layanan Stok
mockLayananStok.cekStok.mengembalikan({ tersedia: true, jumlah: 10 });

// ✅ Uji lulus.
// ❌ Tapi Layanan Stok yang SEBENARNYA sudah lama berubah:
//    { available: true, quantity: 10 }   -- field-nya berganti nama!
// Uji tetap hijau, produksi tetap rusak.

Mock hanya menguji asumsi Anda tentang layanan lain, bukan kenyataannya. Inilah lubang yang ditutup oleh contract testing.

3. Consumer-Driven Contract Testing

1. KONSUMEN (Layanan Pesanan) menuliskan harapannya:
   "Bila saya GET /stok/produk-1,
    saya butuh respons berisi field 'tersedia' (boolean)
    dan 'jumlah' (integer)."

2. Uji konsumen berjalan terhadap mock yang dihasilkan
   dari kontrak itu -> menghasilkan berkas KONTRAK.

3. Kontrak dikirim ke tempat penyimpanan bersama (broker kontrak).

4. PENYEDIA (Layanan Stok) mengambil semua kontrak dari
   seluruh konsumennya, lalu memverifikasi bahwa layanan
   aslinya benar-benar memenuhi semua janji itu.

5. Bila penyedia mengubah nama field, verifikasi GAGAL
   di pipeline penyedia -- SEBELUM konsumen rusak.
💡 Kenapa arahnya dari konsumen

Penyedia sering tidak tahu bagian mana dari responsnya yang benar-benar dipakai. Kalau kontrak ditulis penyedia, ia akan menjanjikan seluruh respons dan tidak bisa mengubah apa pun. Dengan arah dari konsumen, penyedia tahu persis: "tiga konsumen memakai field jumlah, tidak ada yang memakai gudang_id" — sehingga ia bebas mengubah yang tidak dipakai.

Contoh sisi konsumen (gaya Pact)
describe('Kontrak dengan Layanan Stok', () => {
  it('mengembalikan ketersediaan stok', async () => {
    await penyedia.addInteraction({
      state: 'produk-1 tersedia 10 buah',
      uponReceiving: 'permintaan cek stok',
      withRequest: {
        method: 'GET',
        path: '/stok/produk-1'
      },
      willRespondWith: {
        status: 200,
        body: {
          tersedia: like(true),      // yang diuji TIPE-nya,
          jumlah: like(10)           // bukan nilainya
        }
      }
    });

    const hasil = await klienStok.cek('produk-1');
    expect(hasil.tersedia).toBe(true);
  });
});
📌 Uji tipe, bukan nilai

Kontrak yang mengunci nilai persis (jumlah: 10) akan pecah setiap kali data uji berubah. Gunakan pencocok tipe (like, eachLike, regex) agar kontrak memverifikasi bentuk respons — itulah yang sebenarnya menjadi perjanjian antar layanan.

4. Testcontainers — Dependensi Nyata di Uji Integrasi

// Menjalankan PostgreSQL SUNGGUHAN di kontainer,
// otomatis dibersihkan setelah uji selesai
const kontainer = await new PostgreSqlContainer('postgres:17-alpine')
  .withDatabase('uji')
  .start();

const db = buatKoneksi(kontainer.getConnectionUri());
await jalankanMigrasi(db);

// Sekarang uji repository terhadap PostgreSQL asli:
// constraint, tipe data, perilaku transaksi, semuanya nyata.
const pesanan = await repositoriPesanan.simpan({ total: 250000 });
expect(pesanan.id).toBeDefined();

await kontainer.stop();
⚠️ Database in-memory memberi rasa aman yang palsu

Menguji dengan H2 atau SQLite padahal produksi memakai PostgreSQL akan melewatkan seluruh kelas bug: perbedaan tipe data, perilaku constraint, sintaks kueri spesifik, tingkat isolasi transaksi, dan perilaku indeks. Testcontainers menghilangkan celah ini dengan menjalankan mesin database yang sama persis dengan produksi.

5. Menguji Alur Asinkron

it('menerbitkan event PesananDibuat', async () => {
  const broker = await new RabbitMQContainer('rabbitmq:4.1-alpine').start();
  const pesanYangDiterima = [];
  await berlangganan('pesanan.dibuat', (m) => pesanYangDiterima.push(m));

  await layananPesanan.buat({ penggunaId: 55, total: 250000 });

  // JANGAN pakai sleep tetap -- gunakan polling dengan batas waktu
  await tungguSampai(() => pesanYangDiterima.length === 1, { batas: 5000 });

  expect(pesanYangDiterima[0].jenis).toBe('PesananDibuat');
});
🚫 sleep(2000) adalah sumber uji rapuh nomor satu

Di mesin developer yang cepat, 2 detik terlalu lama dan memperlambat pipeline. Di runner CI yang sibuk, 2 detik ternyata kurang dan uji gagal secara acak. Gunakan pola polling sampai kondisi terpenuhi dengan batas waktu, bukan jeda tetap.

6. Yang Perlu Diuji di Tiap Lapisan

LapisanYang diujiTarget waktu
UnitLogika bisnis murni, perhitungan, aturan validasiSeluruhnya < 30 detik
IntegrasiRepository terhadap DB nyata, publisher terhadap broker nyata< 5 menit
KontrakBentuk permintaan/respons antar layanan< 1 menit
KomponenSatu layanan utuh, dependensi eksternal di-stub< 3 menit
E2EHanya 3–5 alur bisnis paling kritis< 15 menit

7. Menguji di Produksi (dengan Aman)

Sebagian keadaan hanya muncul di produksi: data nyata, beban nyata, integrasi pihak ketiga yang sesungguhnya.

  • Synthetic monitoring — jalankan alur kritis secara berkala terhadap produksi, dan beri alert bila gagal.
  • Canary release — arahkan 5% trafik ke versi baru, bandingkan tingkat error dan latensinya sebelum melanjutkan.
  • Shadow traffic — gandakan trafik nyata ke versi baru tanpa memakai responsnya, untuk melihat perilakunya di bawah beban asli.
  • Feature flag — nyalakan fitur baru untuk sekelompok kecil pengguna internal lebih dulu.
📌 Prasyaratnya: kemampuan mundur dengan cepat

Menguji di produksi hanya bertanggung jawab bila Anda bisa mendeteksi masalah dalam hitungan menit (observability yang baik) dan mengembalikan keadaan dalam hitungan detik (feature flag atau rollback otomatis). Tanpa keduanya, itu bukan pengujian — itu berjudi dengan pengguna.